Full Code of jwilber/roughViz for AI

master 17c7ea86bfa2 cached
34 files
367.7 KB
176.1k tokens
145 symbols
1 requests
Download .txt
Showing preview only (382K chars total). Download the full file or copy to clipboard to get everything.
Repository: jwilber/roughViz
Branch: master
Commit: 17c7ea86bfa2
Files: 34
Total size: 367.7 KB

Directory structure:
gitextract_k9obim54/

├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src/
│   ├── Bar.js
│   ├── BarH.js
│   ├── Chart.js
│   ├── Donut.js
│   ├── Force.js
│   ├── Line.js
│   ├── Network.js
│   ├── Pie.js
│   ├── Scatter.js
│   ├── StackedBar.js
│   ├── index.html
│   ├── index.js
│   └── utils/
│       ├── addFonts.js
│       ├── addLegend.js
│       ├── colors.js
│       ├── roughCeiling.js
│       └── saveToPng.js
├── tests/
│   ├── Bar.test.js
│   ├── BarH.test.js
│   ├── Donut.test.js
│   ├── Pie.test.js
│   ├── Scatter.test.js
│   └── utils/
│       └── roughCeiling.test.js
├── vite.config.js
└── website/
    ├── index.html
    ├── main.js
    ├── package.json
    ├── roughDemo.js
    ├── style.css
    └── vite.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
dist/
node_modules
jest.config.js
examples/
.cache
.eslintrc
.vscode/
package-lock.json

================================================
FILE: LICENSE
================================================
Copyright (c) 2020 Jared Wilber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<img src="https://raw.githubusercontent.com/jwilber/random_data/master/roughViz_Title.png"  width="350" alt="roughViz.js"><br>

**roughViz.js** is a reusable JavaScript library for creating sketchy/hand-drawn styled charts in the browser, based on D3v5, roughjs, and handy.


<img src="https://raw.githubusercontent.com/jwilber/random_data/master/roughViz.gif" alt="roughViz.js">


### Why?
Use these charts where the communication goal is to show intent or generality, and not absolute precision. Or just because they're fun and look weird.


### Chart Types

| Chart Type     | API                                                   |
| -------------- | ----------------------------------------------------- |
| Bar            | <a href="#roughvizbar">roughViz.Bar</a>               |
| Horizontal Bar | <a href="#roughvizbarh">roughViz.BarH</a>             |
| Donut          | <a href="#roughvizdonut">roughViz.Donut</a>           |
| Line           | <a href="#roughvizline">roughViz.Line</a>             |
| Pie            | <a href="#roughvizpie">roughViz.Pie</a>               |
| Scatter        | <a href="#roughvizscatter">roughViz.Scatter</a>       |
| Stacked Bar    | <a href="#roughvizstackedbar">roughViz.StackedBar</a> |

Visit [this link](https://observablehq.com/d/6d3209e2f7f114de) for interactive examples of each chart.

### Features

Apply the features of `roughjs` to each chart:

**roughness**:

<img src="https://raw.githubusercontent.com/jwilber/random_data/master/roughViz_roughnessbars.png"  alt="roughness examples">

<b id="fillStyle">fillStyle</b>
<img src="https://raw.githubusercontent.com/jwilber/random_data/master/rough_fillStyles.png"  alt="fillStyle examples">


**fillWeight**
<img src="https://raw.githubusercontent.com/jwilber/random_data/master/roughViz_fillweight.png"  alt="fillStyle examples">


As well as additional chart-specific options ([see API below](#API))


### Installation

Via CDN (expose the `roughViz` global in `html`):

```html
<script src="https://unpkg.com/rough-viz@2.0.5"></script>
```

Via `npm`:

```sh
npm install rough-viz
```
Want to use with `React`? [There's a wrapper!](https://github.com/Chris927/react-roughviz):

```sh
npm install react-roughviz
```

Want to use with `Vue`? [There's a wrapper!](https://github.com/jolo-dev/vue-roughviz):

```sh
npm install vue-roughviz
```

Want to use it with `Python`? [Go crazy](https://github.com/charlesdong1991/py-roughviz):

```sh
pip install py-roughviz
```


### How to use

If you're using ESM, make sure to import the library:

```
import roughViz from "rough-viz";
```

Create some container elements, one for each chart:

```html
<!--you can name each id whatever you want -->
<div id="viz0"></div>
<div id="viz1"></div>
```
In the javascript, just create charts, referencing the desired container:
```js
// create Bar chart from csv file, using default options
 new roughViz.Bar({
    element: '#viz0', // container selection
    data: 'https://raw.githubusercontent.com/jwilber/random_data/master/flavors.csv',
    labels: 'flavor',
    values: 'price'
});

// create Donut chart using defined data & customize plot options
new roughViz.Donut(
  {
    element: '#viz1',
    data: {
      labels: ['North', 'South', 'East', 'West'],
      values: [10, 5, 8, 3]
    },
    title: "Regions",
    width: window.innerWidth / 4,
    roughness: 8,
    colors: ['red', 'orange', 'blue', 'skyblue'],
    stroke: 'black',
    strokeWidth: 3,
    fillStyle: 'cross-hatch',
    fillWeight: 3.5,
  }
);
```

<h2 id="API">API</h2>

### `roughViz.Bar`
Required
- `element` [string]: Id or class of container element.
- `data`: Data with which to construct chart.
Can be either an object or string.

   - If object: must contain `labels` and `values` keys:

    ```js
    new roughViz.Bar({
       element: '.viz',
       data: {labels: ['a', 'b'], values: [10, 20]}
     })
     ```

   - If string: must be a path/url to a `csv` or `tsv`, and you must also specify the `labels` and `values` as separate attributes that represent columns in said file:
   ```js
   new roughViz.Bar({
     element: '#viz0',
     data: 'stringToDataUrl.csv',
     labels: 'nameOfLabelsColumn',
     values: 'nameOfValuesColumn',
   })
   ```

Optional
- `axisFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `axisRoughness` [number]: Roughness for x & y axes. Default: `0.5`.
- `axisStrokeWidth` [number]: Stroke-width for x & y axes. Default: `0.5`.
- `bowing` [number]: Chart bowing. Default: `0`.
- `color` [string]: Color for each bar. Default: `'skyblue'`.
- `fillStyle` [string]: Bar fill-style. Should be one of [fillStyles](#fillStyle) shown above.
- `fillWeight` [number]: Weight of inner paths' color. Default: `0.5`.
- `font`: Font-family to use. You can use `0` or `gaegu` to use `Gaegu`, or `1` or `indie flower` to use `Indie Flower`. Or feed it something else. Default: `Gaegu`.
- `highlight` [string]: Color for each bar on hover. Default: `'coral'`.
- `innerStrokeWidth` [number]: Stroke-width for paths inside bars. Default: `1`.
- `interactive` [boolean]: Whether or not chart is interactive. Default: `true`.
- `labelFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `margin` [object]: Margin object. Default: `{top: 50, right: 20, bottom: 70, left: 100}`
- `padding` [number]: Padding between bars. Default: `0.1`.
- `roughness` [number]: Roughness level of chart. Default: `1`.
- `simplification` [number]: Chart simplification. Default `0.2`.
- `stroke` [string]: Color of bars' stroke. Default: `black`.
- `strokeWidth` [number]: Size of bars' stroke. Default: `1`.
- `title` [string]: Chart title. Optional.
- `titleFontSize` [string]: Font-size for chart title. Default: `'1rem'`.
- `tooltipFontSize` [string]: Font-size for tooltip. Default: `'0.95rem'`.
- `xLabel` [string]: Label for x-axis.
- `yLabel` [string]: Label for y-axis.


### `roughViz.BarH`
Required
- `element` [string]: Id or class of container element.
- `data`: Data with which to construct chart.
Can be either an object or string.

   - If object: must contain `labels` and `values` keys:

    ```js
    new roughViz.BarH({
       element: '.viz',
       data: {labels: ['a', 'b'], values: [10, 20]}
     })
     ```

   - If string: must be a path/url to a `csv` or `tsv`, and you must also specify the `labels` and `values` as separate attributes that represent columns in said file:
   ```js
   new roughViz.BarH({
     element: '#viz0',
     data: 'stringToDataUrl.csv',
     labels: 'nameOfLabelsColumn',
     values: 'nameOfValuesColumn',
   })
   ```

Optional
- `axisFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `axisRoughness` [number]: Roughness for x & y axes. Default: `0.5`.
- `axisStrokeWidth` [number]: Stroke-width for x & y axes. Default: `0.5`.
- `bowing` [number]: Chart bowing. Default: `0`.
- `color` [string]: Color for each bar. Default: `'skyblue'`.
- `fillStyle` [string]: Bar fill-style. Should be one of [fillStyles](#fillStyle) shown above.
- `fillWeight` [number]: Weight of inner paths' color. Default: `0.5`.
- `font`: Font-family to use. You can use `0` or `gaegu` to use `Gaegu`, or `1` or `indie flower` to use `Indie Flower`. Or feed it something else. Default: `Gaegu`.
- `highlight` [string]: Color for each bar on hover. Default: `'coral'`.
- `innerStrokeWidth` [number]: Stroke-width for paths inside bars. Default: `1`.
- `interactive` [boolean]: Whether or not chart is interactive. Default: `true`.
- `labelFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `margin` [object]: Margin object. Default: `{top: 50, right: 20, bottom: 70, left: 100}`
- `padding` [number]: Padding between bars. Default: `0.1`.
- `roughness` [number]: Roughness level of chart. Default: `1`.
- `simplification` [number]: Chart simplification. Default `0.2`.
- `stroke` [string]: Color of bars' stroke. Default: `black`.
- `strokeWidth` [number]: Size of bars' stroke. Default: `1`.
- `title` [string]: Chart title. Optional.
- `titleFontSize` [string]: Font-size for chart title. Default: `'1rem'`.
- `tooltipFontSize` [string]: Font-size for tooltip. Default: `'0.95rem'`.
- `xLabel` [string]: Label for x-axis.
- `yLabel` [string]: Label for y-axis.


### `roughViz.Donut`
Required
- `element` [string]: Id or class of container element.
- `data`: Data with which to construct chart.
Can be either an object or string.

   - If object: must contain `labels` and `values` keys:

    ```js
    new roughViz.Donut({
       element: '.viz',
       data: {labels: ['a', 'b'], values: [10, 20]}
     })
     ```

   - If string: must be a path/url to a `csv`, `json`, or `tsv`, and you must also specify the `labels` and `values` as separate attributes that represent columns in said file:
   ```js
   new roughViz.Donut({
     element: '#viz0',
     data: 'stringToDataUrl.csv',
     labels: 'nameOfLabelsColumn',
     values: 'nameOfValuesColumn',
   })
   ```

Optional
- `bowing` [number]: Chart bowing. Default: `0`.
- `colors` [array]: Array of colors for each arc. Default: `['coral', 'skyblue', '#66c2a5', 'tan', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', 'tan', 'orange']`.
- `fillStyle` [string]: Bar fill-style. Should be one of [fillStyles](#fillStyle) shown above.
- `fillWeight` [number]: Weight of inner paths' color. Default: `0.85`.
- `font`: Font-family to use. You can use `0` or `gaegu` to use `Gaegu`, or `1` or `indie flower` to use `Indie Flower`. Or feed it something else. Default: `Gaegu`.
- `highlight` [string]: Color for each arc on hover. Default: `'coral'`.
- `innerStrokeWidth` [number]: Stroke-width for paths inside arcs. Default: `0.75`.
- `interactive` [boolean]: Whether or not chart is interactive. Default: `true`.
- `legend` [boolean]: Whether or not to add legend. Default: `'true'`.
- `legendPosition` [string]: Position of legend. Should be either `'left'` or `'right'`. Default: `'right'`.
- `margin` [object]: Margin object. Default: `{top: 50, right: 20, bottom: 70, left: 100}`
- `padding` [number]: Padding between bars. Default: `0.1`.
- `roughness` [number]: Roughness level of chart. Default: `1`.
- `simplification` [number]: Chart simplification. Default `0.2`.
- `strokeWidth` [number]: Size of bars' stroke. Default: `1`.
- `title` [string]: Chart title. Optional.
- `titleFontSize` [string]: Font-size for chart title. Default: `'1rem'`.
- `tooltipFontSize` [string]: Font-size for tooltip. Default: `'0.95rem'`.


### `roughViz.Line`
Required
- `element` [string]: Id or class of container element.
- `data`: Must be a path/url to a `csv` or `tsv`, and you must also specify the each `y` as separate attributes that represent columns in said file. Each attribute prefaced with `y` (except `yLabel`) will receive its own line:
   ```js
   new roughViz.Line({
     element: '#viz0',
     data: 'https://raw.githubusercontent.com/jwilber/random_data/master/profits.csv',
     y1: 'revenue',
     y2: 'cost',
     y3: 'profit'
   })
   ```

Optional
- `axisFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `axisRoughness` [number]: Roughness for x & y axes. Default: `0.5`.
- `axisStrokeWidth` [number]: Stroke-width for x & y axes. Default: `0.5`.
- `bowing` [number]: Chart bowing. Default: `0`.
- `circle` [boolean]: Whether or not to add circles to chart. Default: `true`.
- `circleRadius` [number]: Radius of circles. Default: `10`.
- `circleRoughness` [number]: Roughness of circles. Default: `2`.
- `colors` [array or string]: Array of colors for each arc. Default: `['coral', 'skyblue', '#66c2a5', 'tan', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', 'tan', 'orange']`. If string (e.g. `'blue'`), all circles will take that color.
- `fillStyle` [string]: Bar fill-style. Should be one of [fillStyles](#fillStyle) shown above.
- `fillWeight` [number]: Weight of inner paths' color. Default: `0.5`.
- `font`: Font-family to use. You can use `0` or `gaegu` to use `Gaegu`, or `1` or `indie flower` to use `Indie Flower`. Or feed it something else. Default: `Gaegu`.
- `interactive` [boolean]: Whether or not chart is interactive. Default: `true`.
- `labelFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `legend` [boolean]: Whether or not to add legend. Default: `true`.
- `legendPosition` [string]: Position of legend. Should be either `'left'` or `'right'`. Default: `'right'`.
- `margin` [object]: Margin object. Default: `{top: 50, right: 20, bottom: 70, left: 100}`
- `roughness` [number]: Roughness level of chart. Default: `1`.
- `simplification` [number]: Chart simplification. Default `0.2`.
- `stroke` [string]: Color of lines' stroke. Default: `this.colors`.
- `strokeWidth` [number]: Size of lines' stroke. Default: `1`.
- `title` [string]: Chart title. Optional.
- `titleFontSize` [string]: Font-size for chart title. Default: `'0.95rem'`.
- `tooltipFontSize` [string]: Font-size for tooltip. Default: `'0.95rem'`.
- `xLabel` [string]: Label for x-axis.
- `yLabel` [string]: Label for y-axis.


### `roughViz.Pie`
Required
- `element` [string]: Id or class of container element.
- `data`: Data with which to construct chart.
Can be either an object or string.

   - If object: must contain `labels` and `values` keys:

    ```js
    new roughViz.Pie({
       element: '.viz',
       data: {labels: ['a', 'b'], values: [10, 20]}
     })
     ```

   - If string: must be a path/url to a `csv`, `json`, or `tsv`, and you must also specify the `labels` and `values` as separate attributes that represent columns in said file:
   ```js
   new roughViz.Pie({
     element: '#viz0',
     data: 'stringToDataUrl.csv',
     labels: 'nameOfLabelsColumn',
     values: 'nameOfValuesColumn',
   })
   ```

Optional
- `bowing` [number]: Chart bowing. Default: `0`.
- `colors` [array]: Array of colors for each arc. Default: `['coral', 'skyblue', '#66c2a5', 'tan', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', 'tan', 'orange']`.
- `fillStyle` [string]: Bar fill-style. Should be one of [fillStyles](#fillStyle) shown above.
- `fillWeight` [number]: Weight of inner paths' color. Default: `0.85`.
- `font`: Font-family to use. You can use `0` or `gaegu` to use `Gaegu`, or `1` or `indie flower` to use `Indie Flower`. Or feed it something else. Default: `Gaegu`.
- `highlight` [string]: Color for each arc on hover. Default: `'coral'`.
- `innerStrokeWidth` [number]: Stroke-width for paths inside arcs. Default: `0.75`.
- `interactive` [boolean]: Whether or not chart is interactive. Default: `true`.
- `legend` [boolean]: Whether or not to add legend. Default: `true`.
- `legendPosition` [string]: Position of legend. Should be either `'left'` or `'right'`. Default: `'right'`.
- `margin` [object]: Margin object. Default: `{top: 50, right: 20, bottom: 70, left: 100}`
- `padding` [number]: Padding between bars. Default: `0.1`.
- `roughness` [number]: Roughness level of chart. Default: `1`.
- `simplification` [number]: Chart simplification. Default `0.2`.
- `strokeWidth` [number]: Size of bars' stroke. Default: `1`.
- `title` [string]: Chart title. Optional.
- `titleFontSize` [string]: Font-size for chart title. Default: `'1rem'`.
- `tooltipFontSize` [string]: Font-size for tooltip. Default: `'0.95rem'`.


### `roughViz.Scatter`
Required
- `element` [string]: Id or class of container element.
- `data`: Data with which to construct chart.
Can be either an object or string.

   - If object: must contain `x` and `y` keys:

    ```js
    new roughViz.Scatter({
       element: '.viz',
       data: {x: [1, 2, 35], y: [10, 20, 8]}
     })
     ```

   - If string: must be a path/url to a `csv` or `tsv`, and you must also specify the `x` and `y` as separate attributes that represent columns in said file:
   ```js
   new roughViz.Scatter({
     element: '#viz0',
     data: 'stringToDataUrl.csv',
     x: 'nameOfLabelsColumn',
     y: 'nameOfValuesColumn',
   })
   ```

Optional
- `axisFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `axisRoughness` [number]: Roughness for x & y axes. Default: `0.5`.
- `axisStrokeWidth` [number]: Stroke-width for x & y axes. Default: `0.5`.
- `bowing` [number]: Chart bowing. Default: `0`.
- `colors` [array or string]: Array of colors for each arc. Default: `['coral', 'skyblue', '#66c2a5', 'tan', '#8da0cb', '#e78ac3', '#a6d854', '#ffd92f', 'tan', 'orange']`. If string (e.g. `'blue'`), all circles will take that color.
- `colorVar` [string]: If input data is `csv` or `tsv`, this should be an ordinal column with which to color points by.
`curbZero` [boolean]: Whether or not to force (x, y) axes to (0, 0). Default: `false`.
- `fillStyle` [string]: Bar fill-style. Should be one of [fillStyles](#fillStyle) shown above.
- `fillWeight` [number]: Weight of inner paths' color. Default: `0.5`.
- `font`: Font-family to use. You can use `0` or `gaegu` to use `Gaegu`, or `1` or `indie flower` to use `Indie Flower`. Or feed it something else. Default: `Gaegu`.
- `highlight` [string]: Color for each bar on hover. Default: `'coral'`.
- `highlightLabel` [string]: If input data is `csv` or `tsv`, this should be a column representing what value to display on hover. Otherwise, `(x, y)` values will be shown on hover.
- `innerStrokeWidth` [number]: Stroke-width for paths inside circles. Default: `1`.
- `interactive` [boolean]: Whether or not chart is interactive. Default: `true`.
- `labelFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `margin` [object]: Margin object. Default: `{top: 50, right: 20, bottom: 70, left: 100}`
- `radius` [number]: Circle radius. Default: `8`.
- `roughness` [number]: Roughness level of chart. Default: `1`.
- `simplification` [number]: Chart simplification. Default `0.2`.
- `stroke` [string]: Color of circles' stroke. Default: `black`.
- `strokeWidth` [number]: Size of circles' stroke. Default: `1`.
- `title` [string]: Chart title. Optional.
- `titleFontSize` [string]: Font-size for chart title. Default: `'0.95rem'`.
- `tooltipFontSize` [string]: Font-size for tooltip. Default: `'0.95rem'`.
- `xLabel` [string]: Label for x-axis.
- `yLabel` [string]: Label for y-axis.


### `roughViz.StackedBar`
Required
- `element` [string]: Id or class of container element.
- `data`: Data with which to construct chart. Should be an object.
- `labels`: String name of label key in `data` object.

    ```js
    new roughViz.StackedBar({
       element: '#vis0',
       data: [
           {month:'Jan', A:20, B: 5},
           {month:'Feb', A:25, B: 10},
       ],
       labels: 'month',
     })
     ```

Optional
- `axisFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `axisRoughness` [number]: Roughness for x & y axes. Default: `0.5`.
- `axisStrokeWidth` [number]: Stroke-width for x & y axes. Default: `0.5`.
- `bowing` [number]: Chart bowing. Default: `0`.
- `colors` [string]: Array of colors for each bar grouping.
- `fillStyle` [string]: Bar fill-style. Should be one of [fillStyles](#fillStyle) shown above.
- `fillWeight` [number]: Weight of inner paths' color. Default: `0.5`.
- `font`: Font-family to use. You can use `0` or `gaegu` to use `Gaegu`, or `1` or `indie flower` to use `Indie Flower`. Or feed it something else. Default: `Gaegu`.
- `highlight` [string]: Color for each bar on hover. Default: `'coral'`.
- `innerStrokeWidth` [number]: Stroke-width for paths inside bars. Default: `1`.
- `interactive` [boolean]: Whether or not chart is interactive. Default: `true`.
- `labelFontSize` [string]: Font-size for axes' labels. Default: `'1rem'`.
- `margin` [object]: Margin object. Default: `{top: 50, right: 20, bottom: 70, left: 100}`
- `padding` [number]: Padding between bars. Default: `0.1`.
- `roughness` [number]: Roughness level of chart. Default: `1`.
- `simplification` [number]: Chart simplification. Default `0.2`.
- `stroke` [string]: Color of bars' stroke. Default: `black`.
- `strokeWidth` [number]: Size of bars' stroke. Default: `1`.
- `title` [string]: Chart title. Optional.
- `titleFontSize` [string]: Font-size for chart title. Default: `'1rem'`.
- `tooltipFontSize` [string]: Font-size for tooltip. Default: `'0.95rem'`.
- `xLabel` [string]: Label for x-axis.
- `yLabel` [string]: Label for y-axis.



### Contributors
- [Jared Wilber](https://twitter.com/jdwlbr)
- [Laimonas Andriejauskas](https://github.com/laimonasA)
- [Dave Slutzkin](https://github.com/daveslutzkin)
- [JoLo](https://github.com/jolo-dev)
- [Lucas Wilber](https://github.com/lucasjwilber)

### Acknowledgements
This library wouldn't be possible without the following people:

- [Mike Bostock](https://twitter.com/mbostock) for [D3.js](https://d3js.org/).
- [Preet Shihn](https://twitter.com/preetster) for [rough.js](https://roughjs.com/).
- [Jo Wood](https://www.city.ac.uk/people/academics/jo-wood) for [handy](https://www.gicentre.net/software/#/handy/) processing lib.


### License
MIT License

Copyright (c) 2019 Jared Wilber

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: package.json
================================================
{
  "name": "rough-viz",
  "version": "2.0.5",
  "description": "Hand drawn, rough, sketchy data visualization in svg.",
  "jsdelivr": "dist/roughviz.umd.js",
  "main": "dist/roughviz.cjs.js",
  "module": "dist/roughviz.es.js",
  "unpkg": "dist/roughviz.umd.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview",
    "test": "jest --env=jsdom"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/jwilber/roughViz.git"
  },
  "keywords": [
    "chart",
    "graph",
    "rough",
    "hand-drawn",
    "sketchy",
    "dataviz",
    "data visualization"
  ],
  "author": "jwilber",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/jwilber/roughViz/issues"
  },
  "homepage": "https://github.com/jwilber/roughViz#readme",
  "dependencies": {
    "d3-array": "^2.3.1",
    "d3-axis": "^1.0.12",
    "d3-fetch": "^1.1.2",
    "d3-force": "^3.0.0",
    "d3-format": "^1.4.1",
    "d3-scale": "^3.2.0",
    "d3-scale-chromatic": "^1.5.0",
    "d3-selection": "^1.4.0",
    "d3-shape": "^1.3.5",
    "roughjs": "^4.0.0"
  },
  "devDependencies": {
    "eslint": "^6.3.0",
    "eslint-config-strongloop": "^2.1.0",
    "jest": "^24.9.0",
    "rollup-plugin-terser": "^7.0.2",
    "vite": "^4.4.9"
  }
}


================================================
FILE: src/Bar.js
================================================
import { max } from "d3-array";
import { axisBottom, axisLeft } from "d3-axis";
import { csv, tsv } from "d3-fetch";
import { format } from "d3-format";
import { scaleBand, scaleLinear } from "d3-scale";
import { mouse, select, selectAll } from "d3-selection";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { roughCeiling } from "./utils/roughCeiling";

/**
 * Bar chart class, which extends the Chart class.
 */
class Bar extends Chart {
  /**
   * Constructs a new Bar instance.
   * @param {Object} opts - Configuration object for the bar chart.
   */
  constructor(opts) {
    super(opts);

    // load in arguments from config object
    this.data = opts.data;
    this.margin = opts.margin || { top: 20, right: 10, bottom: 20, left: 20 };
    this.color = opts.color || "red";
    this.highlight = opts.highlight || "coral";
    this.roughness = roughCeiling({ roughness: opts.roughness });
    this.stroke = opts.stroke || "black";
    this.strokeWidth = opts.strokeWidth || 1;
    this.axisStrokeWidth = opts.axisStrokeWidth || 0.5;
    this.axisRoughness = opts.axisRoughness || 0.5;
    this.innerStrokeWidth = opts.innerStrokeWidth || 1;
    this.fillWeight = opts.fillWeight || 0.5;
    this.axisFontSize = opts.axisFontSize;
    this.labels = this.dataFormat === "object" ? "labels" : opts.labels;
    this.values = this.dataFormat === "object" ? "values" : opts.values;
    this.xValueFormat = opts.xValueFormat;
    this.yValueFormat = opts.yValueFormat;
    this.padding = opts.padding || 0.1;
    this.xLabel = opts.xLabel || "";
    this.yLabel = opts.yLabel || "";
    this.labelFontSize = opts.labelFontSize || "1rem";
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
    window.addEventListener("resize", this.resizeHandler.bind(this));
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
    select(this.el).select(".tooltip").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.color = opts.color || this.color;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    this.title = opts.title || this.title;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data) {
    if (typeof data === "string") {
      if (data.includes(".csv")) {
        return () => {
          csv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".tsv")) {
        return () => {
          tsv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      }
    } else {
      return () => {
        this.data = data;
        this.drawFromObject();
      };
    }
  }

  /**
   * Created scales required for chart.
   */
  addScales() {
    const that = this;

    this.xScale = scaleBand()
      .rangeRound([0, this.width])
      .padding(this.padding)
      .domain(
        this.dataFormat === "file"
          ? this.data.map((d) => d[that.labels])
          : this.data[that.labels]
      );

    this.yScale = scaleLinear()
      .rangeRound([this.height, 0])
      .domain(
        this.dataFormat === "file"
          ? [0, max(this.data, (d) => +d[that.values])]
          : [0, max(this.data[that.values])]
      );
  }

  /**
   * Create x and y labels for chart.
   */
  addLabels() {
    // xLabel
    if (this.xLabel !== "") {
      this.svg
        .append("text")
        .attr("x", this.width / 2)
        .attr("y", this.height + this.margin.bottom / 2)
        .attr("dx", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.xLabel);
    }
    // yLabel
    if (this.yLabel !== "") {
      this.svg
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - this.margin.left / 1.4)
        .attr("x", 0 - this.height / 2)
        .attr("dy", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.yLabel);
    }
  }

  /**
   * Create x and y axes for chart.
   */
  addAxes() {
    const xAxis = axisBottom(this.xScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.xValueFormat ? format(this.xValueFormat)(d) : d;
      });

    const yAxis = axisLeft(this.yScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.yValueFormat ? format(this.yValueFormat)(d) : d;
      });

    // x-axis
    this.svg
      .append("g")
      .attr("transform", "translate(0," + this.height + ")")
      .call(xAxis)
      .attr("class", `xAxis${this.graphClass}`)
      .selectAll("text")
      .attr("transform", "translate(-10,0)rotate(-45)")
      .style("text-anchor", "end")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.8, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      )
      .style("opacity", 0.9);

    // y-axis
    this.svg
      .append("g")
      .call(yAxis)
      .attr("class", `yAxis${this.graphClass}`)
      .selectAll("text")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      )
      .style("opacity", 0.9);

    // hide original axes
    selectAll("path.domain").attr("stroke", "transparent");
  }

  makeAxesRough(roughSvg, rcAxis) {
    const xAxisClass = `xAxis${this.graphClass}`;
    const yAxisClass = `yAxis${this.graphClass}`;
    const roughXAxisClass = `rough-${xAxisClass}`;
    const roughYAxisClass = `rough-${yAxisClass}`;

    select(`.${xAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughXAxis = rcAxis.path(pathD, {
          fillStyle: "hachure",
        });
        roughXAxis.setAttribute("class", roughXAxisClass);
        roughSvg.appendChild(roughXAxis);
      });
    selectAll(`.${roughXAxisClass}`).attr(
      "transform",
      `translate(0, ${this.height})`
    );

    select(`.${yAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughYAxis = rcAxis.path(pathD, {
          fillStyle: "hachure",
        });
        roughYAxis.setAttribute("class", roughYAxisClass);
        roughSvg.appendChild(roughYAxis);
      });
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 2)
      .attr("class", "title")
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(40, Math.min(this.width, this.height) / 5)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    selectAll(this.interactionG)
      .data(this.dataFormat === "file" ? this.data : this.data.values)
      .append("rect")
      .attr("x", (d, i) => {
        return this.dataFormat === "file"
          ? this.xScale(d[this.labels])
          : this.xScale(this.data[this.labels][i]);
      })
      .attr("y", (d, i) => {
        return this.dataFormat === "file"
          ? this.yScale(+d[this.values])
          : this.yScale(this.data[this.values][i]);
      })
      .attr("width", this.xScale.bandwidth())
      .attr("height", (d, i) => {
        return this.dataFormat === "file"
          ? this.height - this.yScale(+d[this.values])
          : this.height - this.yScale(this.data[this.values][i]);
      })
      .attr("fill", "transparent");

    // create tooltip
    const Tooltip = select(this.el)
      .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("position", "absolute")
      .style("background-color", "white")
      .style("border", "solid")
      .style("border-width", "1px")
      .style("border-radius", "5px")
      .style("padding", "3px")
      .style("font-family", this.fontFamily)
      .style("font-size", this.tooltipFontSize)
      .style("pointer-events", "none");

    // event functions
    let mouseover = function (d) {
      Tooltip.style("opacity", 1);
    };
    const that = this;

    let mousemove = function (d) {
      const attrX = select(this).attr("attrX");
      const attrY = select(this).attr("attrY");
      const mousePos = mouse(this);
      // get size of enclosing div
      Tooltip.html(`<b>${attrX}</b>: ${attrY}`)
        .style("opacity", 0.95)
        .style(
          "transform",
          `translate(${mousePos[0] + 10 + that.margin.left}px, 
              ${
                mousePos[1] -
                10 -
                (that.height + that.margin.top + that.margin.bottom / 2)
              }px)`
        );
    };

    let mouseleave = function (d) {
      Tooltip.style("opacity", 0);
    };

    // d3 event handlers
    selectAll(this.interactionG).on("mouseover", function () {
      mouseover();
      select(this).select("path").style("stroke", that.highlight);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth + 1.2);
    });

    selectAll(this.interactionG).on("mouseout", function () {
      mouseleave();
      select(this).select("path").style("stroke", that.color);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth);
    });

    selectAll(this.interactionG).on("mousemove", mousemove);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: {
        strokeWidth: this.axisStrokeWidth,
        roughness: this.axisRoughness,
      },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        fill: this.color,
        stroke: this.stroke === "none" ? undefined : this.stroke,
        strokeWidth: this.innerStrokeWidth,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();

    // Add barplot
    this.data.values.forEach((d, i) => {
      const node = this.rc.rectangle(
        this.xScale(this.data[this.labels][i]),
        this.yScale(+d),
        this.xScale.bandwidth(),
        this.height - this.yScale(+d),
        {
          simplification: this.simplification,
          fillWeight: this.fillWeight,
        }
      );
      const roughNode = this.roughSvg.appendChild(node);
      roughNode.setAttribute("class", this.graphClass);
      roughNode.setAttribute("attrX", this.data[this.labels][i]);
      roughNode.setAttribute("attrY", +d);
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw

  /**
   * Draw chart from file.
   */
  drawFromFile() {
    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();

    // Add barplot
    this.data.forEach((d) => {
      const node = this.rc.rectangle(
        this.xScale(d[this.labels]),
        this.yScale(+d[this.values]),
        this.xScale.bandwidth(),
        this.height - this.yScale(+d[this.values]),
        {
          simplification: this.simplification,
          fillWeight: this.fillWeight,
        }
      );
      const roughNode = this.roughSvg.appendChild(node);
      roughNode.setAttribute("class", this.graphClass);
      roughNode.setAttribute("attrX", d[this.labels]);
      roughNode.setAttribute("attrY", +d[this.values]);
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw
}

export default Bar;


================================================
FILE: src/BarH.js
================================================
import { max } from "d3-array";
import { axisBottom, axisLeft } from "d3-axis";
import { csv, tsv } from "d3-fetch";
import { format } from "d3-format";
import { scaleBand, scaleLinear } from "d3-scale";
import { mouse, select, selectAll } from "d3-selection";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { roughCeiling } from "./utils/roughCeiling";

/**
 * BarH chart class, which extends the Chart class.
 */
class BarH extends Chart {
  /**
   * Constructs a new BarH instance.
   * @param {Object} opts - Configuration object for the horizontal bar chart.
   */
  constructor(opts) {
    super(opts);

    // load in arguments from config object
    // this.data = opts.data;
    this.margin = opts.margin || { top: 50, right: 20, bottom: 50, left: 100 };
    this.color = opts.color || "red";
    this.highlight = opts.highlight || "coral";
    this.roughness = roughCeiling({ roughness: opts.roughness });
    this.stroke = opts.stroke || "black";
    this.strokeWidth = opts.strokeWidth || 1;
    this.axisStrokeWidth = opts.axisStrokeWidth || 0.5;
    this.axisRoughness = opts.axisRoughness || 0.5;
    this.innerStrokeWidth = opts.innerStrokeWidth || 1;
    this.fillWeight = opts.fillWeight || 0.5;
    this.axisFontSize = opts.axisFontSize;
    this.labels = this.dataFormat === "object" ? "labels" : opts.labels;
    this.values = this.dataFormat === "object" ? "values" : opts.values;
    this.xValueFormat = opts.xValueFormat;
    this.yValueFormat = opts.yValueFormat;
    this.padding = opts.padding || 0.1;
    this.xLabel = opts.xLabel || "";
    this.yLabel = opts.yLabel || "";
    this.labelFontSize = opts.labelFontSize || "1rem";
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
    window.addEventListener("resize", this.resizeHandler.bind(this));
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.stroke = opts.stroke || this.stroke;
    this.color = opts.color || this.color;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data) {
    if (typeof data === "string") {
      if (data.includes(".csv")) {
        return () => {
          csv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".tsv")) {
        return () => {
          tsv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      }
    } else {
      return () => {
        this.data = data;
        this.drawFromObject();
      };
    }
  }

  addScales() {
    const that = this;
    this.yScale = scaleBand()
      .rangeRound([0, this.height])
      .padding(this.padding)
      .domain(
        this.dataFormat === "file"
          ? this.data.map((d) => d[that.labels])
          : this.data[that.labels]
      );

    this.xScale = scaleLinear()
      .rangeRound([0, this.width])
      .domain(
        this.dataFormat === "file"
          ? [0, max(this.data, (d) => +d[that.values])]
          : [0, max(this.data[that.values])]
      );
  }

  /**
   * Create x and y labels for chart.
   */
  addLabels() {
    // xLabel
    if (this.xLabel !== "") {
      this.svg
        .append("text")
        .attr("x", this.width / 2)
        .attr("y", this.height + this.margin.bottom / 2.4)
        .attr("dx", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.xLabel);
    }
    // yLabel
    if (this.yLabel !== "") {
      this.svg
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - this.margin.left / 1.5)
        .attr("x", 0 - this.height / 2)
        .attr("dy", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.yLabel);
    }
  }

  /**
   * Create x and y axes for chart.
   */
  addAxes() {
    const xAxis = axisBottom(this.xScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.xValueFormat ? format(this.xValueFormat)(d) : d;
      });

    const yAxis = axisLeft(this.yScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.yValueFormat ? format(this.yValueFormat)(d) : d;
      });

    // x-axis
    this.svg
      .append("g")
      .attr("transform", `translate(0, ${this.height})`)
      .call(xAxis)
      .attr("class", `xAxis${this.graphClass}`)
      .selectAll("text")
      .attr("transform", "translate(-10,0)rotate(-45)")
      .style("text-anchor", "end")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      )
      .style("opacity", 0.85);

    // y-axis
    this.svg
      .append("g")
      .call(yAxis)
      .attr("class", `yAxis${this.graphClass}`)
      .selectAll("text")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      )
      .style("opacity", 0.85);

    // hide original axes
    selectAll("path.domain").attr("stroke", "transparent");
  }

  makeAxesRough(roughSvg, rcAxis) {
    const xAxisClass = `xAxis${this.graphClass}`;
    const yAxisClass = `yAxis${this.graphClass}`;
    const roughXAxisClass = `rough-${xAxisClass}`;
    const roughYAxisClass = `rough-${yAxisClass}`;

    select(`.${xAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughXAxis = rcAxis.path(pathD, {
          stroke: "black",
          fillStyle: "hachure",
        });
        roughXAxis.setAttribute("class", roughXAxisClass);
        roughSvg.appendChild(roughXAxis);
      });
    selectAll(`.${roughXAxisClass}`).attr(
      "transform",
      `translate(0, ${this.height})`
    );

    select(`.${yAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughYAxis = rcAxis.path(pathD, {
          stroke: "black",
          fillStyle: "hachure",
        });
        roughYAxis.setAttribute("class", roughYAxisClass);
        roughSvg.appendChild(roughYAxis);
      });
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 2)
      .attr("class", "title")
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(40, Math.min(this.width, this.height) / 5)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    // add highlight helper dom nodes
    selectAll(this.interactionG)
      .data(this.dataFormat === "file" ? this.data : this.data.values)
      .append("rect")
      .attr("x", 0)
      .attr("y", (d, i) => {
        return this.dataFormat === "file"
          ? this.yScale(d[this.labels])
          : this.yScale(this.data[this.labels][i]);
      })
      .attr("width", (d, i) => {
        return this.dataFormat === "file"
          ? this.xScale(+d[this.values])
          : this.xScale(this.data[this.values][i]);
      })
      .attr("height", this.yScale.bandwidth())
      .attr("fill", "transparent");

    // create tooltip
    const Tooltip = select(this.el)
      .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("position", "absolute")
      .style("background-color", "white")
      .style("border", "solid")
      .style("border-width", "1px")
      .style("border-radius", "5px")
      .style("padding", "3px")
      .style("font-family", this.fontFamily)
      .style("font-size", this.tooltipFontSize)
      .style("pointer-events", "none");

    // event functions
    let mouseover = function (d) {
      Tooltip.style("opacity", 1);
    };
    const that = this;

    let mousemove = function (d) {
      const attrX = select(this).attr("attrX");
      const attrY = select(this).attr("attrY");
      const mousePos = mouse(this);
      // get size of enclosing div
      Tooltip.html(`<b>${attrX}</b>: ${attrY}`)
        .style("opacity", 0.95)
        .style(
          "transform",
          `translate(${mousePos[0] + that.margin.left}px, 
              ${
                mousePos[1] -
                (that.height + that.margin.top + that.margin.bottom / 2)
              }px)`
        );
    };
    let mouseleave = function (d) {
      Tooltip.style("opacity", 0);
    };

    // d3 event handlers
    selectAll(this.interactionG).on("mouseover", function () {
      mouseover();
      select(this).select("path").style("stroke", that.highlight);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth + 1.2);
    });

    selectAll(this.interactionG).on("mouseout", function () {
      mouseleave();
      select(this).select("path").style("stroke", that.color);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth);
    });

    selectAll(this.interactionG).on("mousemove", mousemove);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: {
        strokeWidth: this.axisStrokeWidth,
        roughness: this.axisRoughness,
      },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        fill: this.color,
        stroke: this.stroke === "none" ? undefined : this.stroke,
        strokeWidth: this.innerStrokeWidth,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();

    this.data.values.forEach((d, i) => {
      const node = this.rc.rectangle(
        0,
        this.yScale(this.data[this.labels][i]),
        this.xScale(d),
        this.yScale.bandwidth(),
        {
          simplification: this.simplification,
          fillWeight: this.fillWeight,
        }
      );
      const roughNode = this.roughSvg.appendChild(node);
      roughNode.setAttribute("class", this.graphClass);
      roughNode.setAttribute("attrX", this.data[this.labels][i]);
      roughNode.setAttribute("attrY", +d);
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw

  /**
   * Draw chart from file.
   */
  drawFromFile() {
    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();

    // Add barplot
    this.data.forEach((d) => {
      const node = this.rc.rectangle(
        0,
        this.yScale(d[this.labels]),
        this.xScale(+d[this.values]),
        this.yScale.bandwidth(),
        {
          simplification: this.simplification,
          fillWeight: this.fillWeight,
        }
      );
      const roughNode = this.roughSvg.appendChild(node);
      roughNode.setAttribute("class", this.graphClass);
      roughNode.setAttribute("attrX", d[this.labels]);
      roughNode.setAttribute("attrY", +d[this.values]);
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw
}

export default BarH;


================================================
FILE: src/Chart.js
================================================
import { select } from "d3-selection";
import { addFontGaegu, addFontIndieFlower } from "./utils/addFonts";

/**
 * Chart class ABC.
 */
class Chart {
  /**
   * Constructs a new Chart instance.
   * @param {Object} opts - Configuration object for the chart.
   */
  constructor(opts) {
    this.el = opts.element;
    this.element = opts.element;
    this.title = opts.title;
    this.titleFontSize = opts.titleFontSize || "17px";
    this.font = opts.font || 0;
    this.fillStyle = opts.fillStyle;
    this.tooltipFontSize = opts.tooltipFontSize || "0.95rem";
    this.bowing = opts.bowing || 0;
    this.simplification = opts.simplification || 0.2;
    this.interactive = opts.interactive !== false;
    this.dataFormat = typeof opts.data === "object" ? "object" : "file";
  }

  setSvg() {
    this.svg = select(this.el)
      .append("svg")
      .attr(
        "viewBox",
        `0 0 ${this.width + this.margin.left + this.margin.right}
       ${this.height + this.margin.top + this.margin.bottom}`
      )
      .append("g")
      .attr("id", this.roughId)
      .attr(
        "transform",
        "translate(" + this.margin.left + "," + this.margin.top + ")"
      );
  }

  resolveFont() {
    if (
      this.font === 0 ||
      this.font === undefined ||
      this.font.toString().toLowerCase() === "gaegu"
    ) {
      addFontGaegu(this.svg);
      this.fontFamily = "gaeguregular";
    } else if (
      this.font === 1 ||
      this.font.toString().toLowerCase() === "indie flower"
    ) {
      addFontIndieFlower(this.svg);
      this.fontFamily = "indie_flowerregular";
    } else {
      this.fontFamily = this.font;
    }
  }
}

export default Chart;


================================================
FILE: src/Donut.js
================================================
import { csv, tsv, json } from "d3-fetch";
import { mouse, select, selectAll } from "d3-selection";
import { arc, pie } from "d3-shape";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { colors } from "./utils/colors";
import { addLegend } from "./utils/addLegend";
import { roughCeiling } from "./utils/roughCeiling";

/**
 * Donut chart class, which extends the Chart class.
 */
class Donut extends Chart {
  /**
   * Constructs a new Donut instance.
   * @param {Object} opts - Configuration object for the donut chart.
   */
  constructor(opts) {
    super(opts);

    // load in arguments from config object
    // this.data = opts.data;
    this.margin = opts.margin || { top: 50, right: 20, bottom: 10, left: 20 };
    this.colors = opts.colors || colors;
    this.highlight = opts.highlight;
    this.roughness = roughCeiling({ roughness: opts.roughness, ceiling: 30 });
    this.strokeWidth = opts.strokeWidth || 0.75;
    this.innerStrokeWidth = opts.innerStrokeWidth || 0.75;
    this.fillWeight = opts.fillWeight || 0.85;
    this.labels = this.dataFormat === "object" ? "labels" : opts.labels;
    this.values = this.dataFormat === "object" ? "values" : opts.values;
    if (this.labels === undefined || this.values === undefined) {
      console.log(`Error for ${this.el}: Must include labels and values when \
       instantiating Donut chart. Skipping chart.`);
      return;
    }
    this.legend = opts.legend !== false;
    this.legendPosition = opts.legendPosition || "right";
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
    window.addEventListener("resize", this.resizeHandler.bind(this));
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }
  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }
  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.radius = Math.min(this.width, this.height) / 2;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data) {
    if (typeof data === "string") {
      if (data.includes(".csv")) {
        return () => {
          csv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".tsv")) {
        return () => {
          tsv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".json")) {
        return () => {
          json(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      }
    } else {
      return () => {
        this.data = data;
        this.drawFromObject();
      };
    }
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 3)
      .attr("class", "title")
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(40, Math.min(this.width, this.height) / 4)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    selectAll(this.interactionG)
      .append("g")
      .attr("transform", `translate(${this.width / 2}, ${this.height / 2})`)
      .data(
        this.dataFormat === "object"
          ? this.makePie(this.data[this.values])
          : this.makePie(this.data)
      )
      .append("path")
      .attr("d", this.makeArc)
      .attr("stroke-width", "0px")
      .attr("fill", "transparent");

    // create tooltip
    const Tooltip = select(this.el)
      .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("position", "absolute")
      .style("background-color", "white")
      .style("border", "solid")
      .style("border-width", "1px")
      .style("border-radius", "5px")
      .style("padding", "3px")
      .style("font-family", this.fontFamily)
      .style("font-size", this.tooltipFontSize)
      .style("pointer-events", "none");

    // event functions
    let mouseover = function (d) {
      Tooltip.style("opacity", 1);
    };

    const that = this;
    let thisColor;

    let mousemove = function (d) {
      const attrX = select(this).attr("attrX");
      const attrY = select(this).attr("attrY");
      const mousePos = mouse(this);
      // get size of enclosing div
      Tooltip.html(`<b>${attrX}</b>: ${attrY}`)
        .style("opacity", 0.95)
        .style(
          "transform",
          `translate(${mousePos[0] + that.margin.left}px, 
              ${
                mousePos[1] -
                (that.height + that.margin.top + that.margin.bottom / 2)
              }px)`
        );
    };
    let mouseleave = function (d) {
      Tooltip.style("opacity", 0);
    };

    // d3 event handlers
    selectAll(this.interactionG).on("mouseover", function () {
      mouseover();
      thisColor = select(this).selectAll("path").style("stroke");
      that.highlight === undefined
        ? select(this).selectAll("path").style("opacity", 0.5)
        : select(this).selectAll("path").style("stroke", that.highlight);
    });

    selectAll(this.interactionG).on("mouseout", function () {
      mouseleave();
      select(this).selectAll("path").style("stroke", thisColor);
      select(this).selectAll("path").style("opacity", 1);
    });

    selectAll(this.interactionG).on("mousemove", mousemove);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: { strokeWidth: this.strokeWidth >= 3 ? 3 : this.strokeWidth },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        fill: this.color,
        strokeWidth: this.innerStrokeWidth,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
        fillWeight: this.fillWeight,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    this.initRoughObjects();

    this.makePie = pie();

    this.makeArc = arc().innerRadius(0).outerRadius(this.radius);

    this.arcs = this.makePie(this.data[this.values]);

    this.arcs.forEach((d, i) => {
      if (d.value !== 0) {
        const node = this.rc.arc(
          this.width / 2, // x
          this.height / 2, // y
          2 * this.radius, // width
          2 * this.radius, // height
          d.startAngle - Math.PI / 2, // start
          d.endAngle - Math.PI / 2, // stop
          true,
          {
            fill: this.colors[i],
            stroke: this.colors[i],
          }
        );
        node.setAttribute("class", this.graphClass);
        const roughNode = this.roughSvg.appendChild(node);
        roughNode.setAttribute("attrY", this.data[this.values][i]);
        roughNode.setAttribute("attrX", this.data[this.labels][i]);
      }
    });

    const donutNode = this.rc.circle(
      this.width / 2,
      this.height / 2,
      this.radius,
      {
        fill: "white",
        strokeWidth: 0.05,
        fillWeight: 10,
        fillStyle: "solid",
      }
    );
    this.roughSvg.appendChild(donutNode);

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);

    // ADD LEGEND
    const dataSources = this.data.labels;
    const legendItems = dataSources.map((key, i) => ({
      color: this.colors[i],
      text: key,
    }));
    // find length of longest text item
    const legendWidth =
      legendItems.reduce(
        (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
        0
      ) *
        6 +
      35;
    const legendHeight = legendItems.length * 11 + 8;

    if (this.legend === true) {
      addLegend(this, legendItems, legendWidth, legendHeight);
    }

    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  }

  /**
   * Draw chart from file.
   */
  drawFromFile() {
    this.initRoughObjects();

    this.makePie = pie()
      .value((d) => d[this.values])
      .sort(null);

    const valueArr = [];
    this.makeArc = arc().innerRadius(0).outerRadius(this.radius);

    this.arcs = this.makePie(this.data);

    this.arcs.forEach((d, i) => {
      if (d.value !== 0) {
        const node = this.rc.arc(
          this.width / 2, // x
          this.height / 2, // y
          2 * this.radius, // width
          2 * this.radius, // height
          d.startAngle - Math.PI / 2, // start
          d.endAngle - Math.PI / 2, // stop
          true,
          {
            fill: this.colors[i],
            stroke: this.colors[i],
          }
        );
        node.setAttribute("class", this.graphClass);
        const roughNode = this.roughSvg.appendChild(node);
        roughNode.setAttribute("attrY", d.data[this.values]);
        roughNode.setAttribute("attrX", d.data[this.labels]);
      }
      valueArr.push(d.data[this.labels]);
    });

    const donutNode = this.rc.circle(
      this.width / 2,
      this.height / 2,
      this.radius,
      {
        fill: "white",
        strokeWidth: 0.05,
        fillWeight: 10,
        fillStyle: "solid",
      }
    );
    this.roughSvg.appendChild(donutNode);

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);

    // ADD LEGEND
    const dataSources = valueArr;
    const legendItems = dataSources.map((key, i) => ({
      color: this.colors[i],
      text: key,
    }));
    // find length of longest text item
    const legendWidth =
      legendItems.reduce(
        (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
        0
      ) *
        6 +
      35;
    const legendHeight = legendItems.length * 11 + 8;

    if (this.legend === true) {
      addLegend(this, legendItems, legendWidth, legendHeight);
    }

    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw
}

export default Donut;


================================================
FILE: src/Force.js
================================================
import { mouse, select, selectAll } from "d3-selection";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { colors } from "./utils/colors";
import { addLegend } from "./utils/addLegend";
import { roughCeiling } from "./utils/roughCeiling";
import { forceSimulation, forceCollide, forceCenter } from "d3-force";
import { min, max } from "d3-array";
import { scaleLinear } from "d3-scale";

/**
 * Force chart class, which extends the Chart class.
 */
class Force extends Chart {
  /**
   * Constructs a new Force instance.
   * @param {Object} opts - Configuration object for the force chart.
   */
  constructor(opts) {
    super(opts);
    this.data = opts.data;
    this.margin = opts.margin || { top: 50, right: 20, bottom: 10, left: 20 };
    this.colors = opts.colors || colors;
    this.highlight = opts.highlight;
    this.roughness = roughCeiling({
      roughness: opts.roughness,
      ceiling: 30,
      defaultValue: 0,
    });
    this.strokeWidth = opts.strokeWidth || 0.75;
    this.innerStrokeWidth = opts.innerStrokeWidth || 0.75;
    this.fillWeight = opts.fillWeight || 0.85;
    this.color = opts.color || "pink";
    this.collision = opts.collision || 1;
    this.radiusExtent = opts.radiusExtent || [5, 20];
    this.radius = opts.radius || "radius";
    this.roughnessExtent = opts.roughnessExtent || [0, 10];
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    const defaultTextCallback = (d) => "";
    this.textCallback = opts.textCallback || defaultTextCallback;
    const defaultColorCallback = (d) => this.color;
    this.colorCallback = opts.colorCallback || defaultColorCallback;
    this.legend = opts.legend || false;
    this.legendPosition = opts.legendPosition || "right";
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.collision = opts.collision || this.collision;
    this.color = opts.color || this.color;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    this.title = opts.title || this.title;
    const defaultTextCallback = (d) => "";
    this.textCallback = opts.textCallback || defaultTextCallback;

    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data) {
    return () => {
      this.data = data;
      this.drawFromObject();
    };
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 3)
      .attr("class", "title")
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(40, Math.min(this.width, this.height) / 4)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    const that = this;
    let thisColor;

    let mouseleave = function (d) {
      select(this).selectAll("path:nth-child(1)").style("opacity", 1);
      select(this).selectAll("path:nth-child(1)").style("stroke", thisColor);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth);

      select(this).select(".node-text").attr("opacity", 0);
    };

    let mouseover = function (d) {
      thisColor = select(this).selectAll("path").style("stroke");
      select(this).raise();
      that.highlight === undefined
        ? select(this).selectAll("path:nth-child(1)").style("opacity", 0.4)
        : select(this)
            .selectAll("path:nth-child(1)")
            .style("stroke", that.highlight);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth + 1.2);

      select(this).select(".node-text").attr("opacity", 1);

      select(this).select(".node-text").raise();
    };

    selectAll(".nodeGroup")
      .on("mouseover", mouseover)
      .on("mouseleave", mouseleave);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: { strokeWidth: this.strokeWidth >= 3 ? 3 : this.strokeWidth },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        strokeWidth: this.innerStrokeWidth,
        fill: this.color,
        stroke: this.stroke === "none" ? undefined : this.stroke,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    const that = this;
    let radiusScale;
    let roughnessScale;

    if (typeof this.radius === "number") {
      radiusScale = scaleLinear()
        .domain([0, 1])
        .range([this.radiusExtent[0], this.radiusExtent[1]]);
    } else {
      const dataMin = min(this.data, (d) => +d[this.radius]);
      const dataMax = max(this.data, (d) => +d[this.radius]);

      // Create a scale based on data's min and max values
      radiusScale = scaleLinear()
        .domain([dataMin, dataMax])
        .range([this.radiusExtent[0], this.radiusExtent[1]]);
    }

    if (typeof this.roughness === "number") {
      roughnessScale = scaleLinear()
        .domain([0, 1])
        .range([this.roughnessExtent[0], this.roughnessExtent[1]]);
    } else {
      const roughnessMin = min(this.data, (d) => +d[this.radius]);
      const roughnessMax = max(this.data, (d) => +d[this.radius]);

      // Create a scale based on data's min and max values
      roughnessScale = scaleLinear()
        .domain([roughnessMin, roughnessMax])
        .range([this.roughnessExtent[0], this.roughnessExtent[1]]);
    }

    this.initRoughObjects();

    let nodeGroups = this.svg.selectAll(".nodeGroup").data(this.data);

    let nodeGroupsEnter = nodeGroups
      .enter()
      .append("g")
      .attr("class", "nodeGroup");

    nodeGroups = nodeGroups.merge(nodeGroupsEnter);

    nodeGroups.each(function (d, i) {
      const nodeRadius =
        typeof that.radius === "number"
          ? that.radius
          : radiusScale(d[that.radius]);

      const nodeRoughness =
        typeof that.roughness === "number"
          ? that.roughness
          : roughnessScale(d[that.roughness]);

      const node = that.rc.circle(0, 0, nodeRadius, {
        fill: that.colorCallback(d),
        simplification: that.simplification,
        fillWeight: that.fillWeight,
        roughness: nodeRoughness,
      });

      const roughNode = this.appendChild(node);
      roughNode.setAttribute("class", that.graphClass + "_node");

      select(this)
        .append("circle")
        .attr("class", "node-circle")
        .attr("r", nodeRadius * 0.5)
        .attr("fill", "transparent")
        .attr("stroke-width", 0)
        .attr("stroke", "none");

      select(this)
        .append("text")
        .attr("class", "node-text")
        .attr("x", 0)
        .attr("y", -10) // Adjust 15 based on your needs
        .attr("text-anchor", "middle")
        .style("pointer-events", "none")
        .attr("stroke", "black")
        .attr("fill", "white")
        .attr("stroke-linejoin", "fill")
        .attr("paint-order", "stroke fill")
        .attr("stroke-width", "5px")
        .attr("opacity", 0)
        .text((d) => that.textCallback(d));
    });

    const simulation = forceSimulation(this.data);
    simulation.alpha(1).restart();

    simulation
      .force(
        "collide",
        forceCollide().radius((d) => d.radius * this.collision * 1.2)
      )
      .force("center", forceCenter(this.width / 2, this.height / 2));

    simulation.on("tick", () => {
      nodeGroups.attr("transform", (d) => `translate(${d.x}, ${d.y})`);
      nodeGroups.attr("attrX", (d) => +d.x);
      nodeGroups.attr("attrY", (d) => +d.y);
    });

    selectAll(".nodeGroup")
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);

    if (this.interactive === true) {
      this.addInteraction();
    }

    if (this.legend) {
      const legendItems = this.legend;
      this.colors = this.legend.map((item) => item.color);

      const legendWidth =
        legendItems.reduce(
          (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
          0
        ) *
          6 +
        35;
      const legendHeight = legendItems.length * 11 + 8;

      addLegend(this, legendItems, legendWidth, legendHeight);
    }
  }
}

export default Force;


================================================
FILE: src/Line.js
================================================
import { bisect, extent, max, min, range } from "d3-array";
import { axisBottom, axisLeft } from "d3-axis";
import { csv, tsv } from "d3-fetch";
import { format } from "d3-format";
import { scaleLinear, scalePoint } from "d3-scale";
import { mouse, select, selectAll } from "d3-selection";
import { line } from "d3-shape";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { addLegend } from "./utils/addLegend";
import { colors } from "./utils/colors";
import { roughCeiling } from "./utils/roughCeiling";

const allDataExtent = (data) => {
  // get extend for all keys in data
  const keys = Object.keys(data);
  const extents = keys.map((key) => extent(data[key]));
  const dataMin = min(extents, (d) => d[0]);
  const dataMax = max(extents, (d) => d[1]);
  return [dataMin, dataMax];
};

/**
 * Line chart class, which extends the Chart class.
 */
class Line extends Chart {
  /**
   * Constructs a new Line instance.
   * @param {Object} opts - Configuration object for the line chart.
   */
  constructor(opts) {
    super(opts);

    // load in arguments from config object
    this.margin = opts.margin || { top: 50, right: 20, bottom: 50, left: 100 };
    this.roughness = roughCeiling({
      roughness: opts.roughness,
      defaultValue: 2.2,
    });
    this.axisStrokeWidth = opts.axisStrokeWidth || 0.5;
    this.axisRoughness = opts.axisRoughness || 0.5;
    this.stroke = opts.stroke || "black";
    this.fillWeight = opts.fillWeight || 0.5;
    this.colors = opts.colors;
    this.strokeWidth = opts.strokeWidth || 1;
    this.axisFontSize = opts.axisFontSize;
    this.x = opts.x;
    this.y = this.dataFormat === "object" ? "y" : opts.y;
    this.xValueFormat = opts.xValueFormat;
    this.yValueFormat = opts.yValueFormat;
    this.legend = opts.legend !== false;
    this.legendPosition = opts.legendPosition || "right";
    this.circle = opts.circle !== false;
    this.circleRadius = opts.circleRadius || 10;
    this.circleRoughness = roughCeiling({
      roughness: opts.circleRoughness,
      defaultValue: 2,
    });
    this.xLabel = opts.xLabel || "";
    this.yLabel = opts.yLabel || "";
    this.labelFontSize = opts.labelFontSize || "1rem";
    if (this.dataFormat === "file") {
      this.dataSources = [];
      this.yKeys = Object.keys(opts).filter((name) => /y/.test(name));
      this.yKeys.map((key, i) => {
        if (key !== "yLabel") this.dataSources.push(opts[key]);
      });
    }
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
    window.addEventListener("resize", this.resizeHandler.bind(this));
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data) {
    if (typeof data === "string") {
      if (data.includes(".csv")) {
        return () => {
          csv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".tsv")) {
        return () => {
          tsv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      }
    } else {
      return () => {
        this.data = data;
        this.drawFromObject();
      };
    }
  }

  addScales() {
    let dataExtent;
    if (this.dataFormat !== "file") {
      dataExtent = allDataExtent(this.data);
    } else {
      const extents = this.dataSources.map((key) =>
        extent(this.data, (d) => +d[key])
      );
      const dataMin = min(extents, (d) => d[0]);
      const dataMax = max(extents, (d) => d[1]);
      dataExtent = [dataMin, dataMax];
    }
    // get value domains and pad axes by 5%
    // if this.x is undefined, use index for x
    let xExtent;
    if (this.x === undefined) {
      // get length of longest array
      const keys = Object.keys(this.data);
      const lengths = keys.map((key) => this.data[key].length);
      const maxLen = max(lengths);
      // Need to make xScale, when this.x is given, ordinal.
      xExtent =
        this.dataFormat === "file" ? [0, this.data.length] : [0, maxLen];
    } else {
      xExtent = extent(this.x);
    }

    const yExtent = dataExtent;

    const yRange = yExtent[1] - yExtent[0];

    this.xScale =
      this.x === undefined
        ? scalePoint()
            .range([0, this.width])
            .domain([...Array(xExtent[1]).keys()])
        : scalePoint().range([0, this.width]).domain(this.x);

    this.yScale = scaleLinear()
      .range([this.height, 0])
      .domain([0, yExtent[1] + yRange * 0.05]);
  }

  /**
   * Create x and y labels for chart.
   */
  addLabels() {
    // xLabel
    if (this.xLabel !== "") {
      this.svg
        .append("text")
        .attr("x", this.width / 2)
        .attr("y", this.height + this.margin.bottom / 1.3)
        .attr("dx", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.xLabel);
    }
    // yLabel
    if (this.yLabel !== "") {
      this.svg
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - this.margin.left / 2)
        .attr("x", 0 - this.height / 2)
        .attr("dy", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.yLabel);
    }
  }

  /**
   * Create x and y axes for chart.
   */
  addAxes() {
    const xAxis = axisBottom(this.xScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.xValueFormat ? format(this.xValueFormat)(d) : d;
      });

    const yAxis = axisLeft(this.yScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.yValueFormat ? format(this.yValueFormat)(d) : d;
      });

    // x-axis
    this.svg
      .append("g")
      .attr("transform", "translate(0," + this.height + ")")
      .call(xAxis)
      .attr("class", `xAxis${this.graphClass}`)
      .selectAll("text")
      .attr("transform", "translate(-10, 0)rotate(-45)")
      .style("text-anchor", "end")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      );

    // y-axis
    this.svg
      .append("g")
      .call(yAxis)
      .attr("class", `yAxis${this.graphClass}`)
      .selectAll("text")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      );

    // hide original axes
    selectAll("path.domain").attr("stroke", "transparent");

    selectAll("g.tick").style("opacity", 1);
  }

  makeAxesRough(roughSvg, rcAxis) {
    const xAxisClass = `xAxis${this.graphClass}`;
    const yAxisClass = `yAxis${this.graphClass}`;
    const roughXAxisClass = `rough-${xAxisClass}`;
    const roughYAxisClass = `rough-${yAxisClass}`;

    select(`.${xAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughXAxis = rcAxis.path(pathD, {
          stroke: "black",
          fillStyle: "hachure",
        });
        roughXAxis.setAttribute("class", roughXAxisClass);
        roughSvg.appendChild(roughXAxis);
      });
    selectAll(`.${roughXAxisClass}`).attr(
      "transform",
      `translate(0, ${this.height})`
    );

    select(`.${yAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughYAxis = rcAxis.path(pathD, {
          stroke: "black",
          fillStyle: "hachure",
        });
        roughYAxis.setAttribute("class", roughYAxisClass);
        roughSvg.appendChild(roughYAxis);
      });
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 2)
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(20, Math.min(this.width, this.height) / 4)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    const that = this;
    this.chartScreen = this.svg.append("g").attr("pointer-events", "all");

    this.dataSources.map((key, idx) => {
      const yValues = this.dataFormat === "file" ? this.data : this.data[key];
      const points = yValues.map((d, i) => {
        return this.x === undefined
          ? [this.xScale(i), this.yScale(d[key])]
          : [this.xScale(this.x[i]), this.yScale(+d[key])];
      });

      // remove undefined elements so no odd behavior
      const drawPoints = points.filter((d) => d[0] !== undefined);

      const lineGen = line()
        .x((d) => d[0])
        .y((d) => d[1]);

      // create lines
      this.svg
        .append("path")
        .datum(drawPoints)
        .attr("fill", "none")
        .attr("stroke", "blue")
        .attr("stroke-width", 1.5)
        .attr("d", lineGen)
        .attr("visibility", "hidden");

      // create tracking class (for interaction)
      const iClass = key + "class";

      // create hover text
      this.svg
        .append("g")
        .attr("class", iClass + "text")
        .append("text")
        .style("font-size", this.tooltipFontSize)
        .style("opacity", 0)
        .style("font-family", this.fontFamily)
        .attr("text-anchor", "middle")
        .attr("alignment-baseline", "middle");
    });

    const mousemove = function (d) {
      // recover coordinate we need
      const xPos = mouse(this)[0];
      const domain = that.xScale.domain();
      const xRange = that.xScale.range();
      const rangePoints = range(xRange[0], xRange[1] + 1, that.xScale.step());
      const xSpot = bisect(rangePoints, xPos);
      const yPos = domain[xSpot];

      that.dataSources.map((key, i) => {
        const hoverData =
          that.dataFormat === "file"
            ? that.x === undefined
              ? that.data[yPos]
              : that.data[xSpot]
            : that.data[key][xSpot];
        // resolve select classes for hover effects
        const thatClass = "." + key + "class";
        const textClass = thatClass + "text";

        if (that.dataFormat === "file") {
          select(textClass)
            .selectAll("text")
            .style("opacity", 1)
            .html(
              that.x === undefined
                ? `(${xSpot},${hoverData[key]})`
                : `(${that.x[xSpot]}, ${hoverData[key]})`
            )
            .attr(
              "x",
              that.x === undefined
                ? that.xScale(xSpot)
                : that.xScale(that.x[xSpot])
            )
            .attr("y", that.yScale(hoverData[key]) - 6);
        } else {
          select(textClass)
            .selectAll("text")
            .style("opacity", 1)
            .html(
              that.x === undefined
                ? `(${xSpot}, ${hoverData})`
                : `(${that.x[xSpot]}, ${hoverData})`
            )
            .attr(
              "x",
              that.x === undefined
                ? that.xScale(xSpot)
                : that.xScale(that.x[xSpot])
            )
            .attr("y", that.yScale(hoverData));
        }
      });
    };

    this.chartScreen
      .append("rect")
      .attr("width", this.width)
      .attr("height", this.height)
      .attr("fill", "none")
      .on("mousemove", mousemove)
      .on("mouseout", () => {
        that.dataSources.map((key) => {
          const thatClass = "." + key + "class";
          const textClass = thatClass + "text";
          select(textClass).selectAll("text").style("opacity", 0);
        });
      });
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: {
        strokeWidth: this.axisStrokeWidth,
        roughness: this.axisRoughness,
      },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        stroke: this.stroke === "none" ? undefined : this.stroke,
        strokeWidth: this.strokeWidth,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    const that = this;
    // set default color
    if (this.colors === undefined) this.colors = colors;

    this.dataSources = Object.keys(this.data);
    this.initRoughObjects();
    this.addScales();
    this.dataSources.map((key, idx) => {
      const points = this.data[key].map((d, i) => {
        return this.x === undefined
          ? [this.xScale(i), this.yScale(+d)]
          : [this.xScale(this.x[i]), this.yScale(d)];
      });

      // remove undefined elements so no odd behavior
      const drawPoints = points.filter((d) => d[0] !== undefined);
      const node = this.rc.curve(drawPoints, {
        stroke: that.colors.length === 1 ? that.colors[0] : that.colors[idx],
        roughness: that.roughness,
        bowing: that.bowing,
      });

      const roughNode = this.roughSvg.appendChild(node);
      roughNode.setAttribute("class", this.graphClass);
      if (this.circle === true) {
        points.forEach((d, i) => {
          const node = this.rc.circle(d[0], d[1], this.circleRadius, {
            stroke: this.colors[idx],
            fill: this.colors[idx],
            fillStyle: "solid",
            strokeWidth: 1,
            roughness: this.circleRoughness,
          });
          this.roughSvg.appendChild(node);
        });
      }
    });
    // ADD LEGEND
    const legendItems = this.dataSources.map((key, i) => ({
      color: this.colors[i],
      text: key,
    }));
    // find length of longest text item
    const legendWidth =
      legendItems.reduce(
        (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
        0
      ) *
        6 +
      35;
    const legendHeight = legendItems.length * 11 + 8;

    if (this.legend === true) {
      addLegend(this, legendItems, legendWidth, legendHeight, 2);
    }

    this.addAxes();
    this.addLabels();
    this.makeAxesRough(this.roughSvg, this.rcAxis);

    if (this.interactive === true) {
      this.addInteraction();
    }
  }

  /**
   * Draw chart from file.
   */
  drawFromFile() {
    // set default colors
    if (this.colors === undefined) this.colors = colors;

    this.initRoughObjects();
    this.addScales();

    // Add scatterplot
    this.dataSources.map((key, idx) => {
      const points = this.data.map((d, i) => {
        return this.x === undefined
          ? [this.xScale(i), this.yScale(d[key])]
          : [this.xScale(this.x[i]), this.yScale(+d[key])];
      });

      // remove undefined elements so no odd behavior
      const drawPoints = points.filter((d) => d[0] !== undefined);
      const node = this.rc.curve(drawPoints, {
        stroke: this.colors[idx],
        strokeWidth: this.strokeWidth,
        roughness: 1,
        bowing: 10,
      });

      this.roughSvg.appendChild(node);
      if (this.circle === true) {
        drawPoints.forEach((d, i) => {
          const node = this.rc.circle(d[0], d[1], this.circleRadius, {
            stroke: this.colors[idx],
            fill: this.colors[idx],
            fillStyle: "solid",
            strokeWidth: 1,
            roughness: this.circleRoughness,
          });
          this.roughSvg.appendChild(node);
        });
      }
    });

    // ADD LEGEND
    const legendItems = this.dataSources.map((key, i) => ({
      color: this.colors[i],
      text: key,
    }));
    // find length of longest text item
    const legendWidth =
      legendItems.reduce(
        (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
        0
      ) *
        6 +
      35;
    const legendHeight = legendItems.length * 11 + 8;
    if (this.legend === true) {
      addLegend(this, legendItems, legendWidth, legendHeight, 2);
    }

    this.addAxes();
    this.addLabels();
    this.makeAxesRough(this.roughSvg, this.rcAxis);

    if (this.interactive === true) {
      this.addInteraction();
    }
  }
}

export default Line;


================================================
FILE: src/Network.js
================================================
import { csv, tsv, json } from "d3-fetch";
import { mouse, select, selectAll } from "d3-selection";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { colors } from "./utils/colors";
import { addLegend } from "./utils/addLegend";
import { roughCeiling } from "./utils/roughCeiling";
import { min, max } from "d3-array";
import { scaleLinear } from "d3-scale";
import {
  forceSimulation,
  forceCollide,
  forceCenter,
  forceLink,
} from "d3-force";

/**
 * Network chart class, which extends the Chart class.
 */
class Network extends Chart {
  /**
   * Constructs a new Network instance.
   * @param {Object} opts - Configuration object for the network chart.
   */
  constructor(opts) {
    super(opts);
    // load in arguments from config object
    this.data = opts.data;
    this.links = opts.links;
    this.margin = opts.margin || { top: 50, right: 20, bottom: 10, left: 20 };
    this.colors = opts.colors || colors;
    this.highlight = opts.highlight;
    this.roughness = roughCeiling({
      roughness: opts.roughness,
      ceiling: 30,
      defaultValue: 0,
    });
    this.strokeWidth = opts.strokeWidth || 0.75;
    this.innerStrokeWidth = opts.innerStrokeWidth || 0.75;
    this.fillWeight = opts.fillWeight || 0.85;
    this.color = opts.color || "skyblue";
    this.collision = opts.collision || 1.4;
    this.radiusExtent = opts.radiusExtent || [5, 20];
    this.radius = opts.radius || "radius";
    const defaultTextCallback = (d) => "";
    this.textCallback = opts.textCallback || defaultTextCallback;
    const defaultColorCallback = (d) => this.color;
    this.colorCallback = opts.colorCallback || defaultColorCallback;
    this.roughnessExtent = opts.roughnessExtent || [0, 10];
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    this.legend = opts.legend || false;
    this.legendPosition = opts.legendPosition || "right";
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data, opts.links);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data, opts.links);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.collision = opts.collision || this.collision;
    this.color = opts.color || this.color;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    this.title = opts.title || this.title;
    const defaultTextCallback = (d) => "";
    this.textCallback = opts.textCallback || defaultTextCallback;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data, links) {
    return () => {
      this.data = data;
      this.links = links;
      this.drawFromObject();
    };
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 3)
      .attr("class", "title")
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(40, Math.min(this.width, this.height) / 4)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    const that = this;
    let thisColor;

    let mouseleave = function (d) {
      select(this).selectAll("path:nth-child(1)").style("opacity", 1);
      select(this).selectAll("path:nth-child(1)").style("stroke", thisColor);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.innerStrokeWidth);

      select(this).select(".node-text").attr("opacity", 0);
    };

    let mouseover = function (d) {
      select(this).raise();
      thisColor = select(this).selectAll("path").style("stroke");
      that.highlight === undefined
        ? select(this).selectAll("path:nth-child(1)").style("opacity", 0.4)
        : select(this)
            .selectAll("path:nth-child(1)")
            .style("stroke", that.highlight);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth + 1.2);

      select(this).select(".node-text").attr("opacity", 1);
    };

    // selectAll(this.interactionG).on("mousemove", mousemove);
    selectAll(".nodeGroup")
      .on("mouseover", mouseover)
      .on("mouseleave", mouseleave);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: { strokeWidth: this.strokeWidth >= 3 ? 3 : this.strokeWidth },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        strokeWidth: this.innerStrokeWidth,
        fill: this.color,
        stroke: this.stroke === "none" ? undefined : this.stroke,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    const that = this;
    let radiusScale;
    let roughnessScale;

    if (typeof this.radius === "number") {
      radiusScale = scaleLinear()
        .domain([0, 1])
        .range([this.extent[0], this.radiusExtent[1]]);
    } else {
      const dataMin = min(this.data, (d) => +d[this.radius]);
      const dataMax = max(this.data, (d) => +d[this.radius]);

      // Create a scale based on data's min and max values
      radiusScale = scaleLinear()
        .domain([dataMin, dataMax])
        .range([this.radiusExtent[0], this.radiusExtent[1]]);
    }

    if (typeof this.roughness === "number") {
      roughnessScale = scaleLinear()
        .domain([0, 1])
        .range([this.roughnessExtent[0], this.roughnessExtent[1]]);
    } else {
      const roughnessMin = min(this.data, (d) => +d[this.radius]);
      const roughnessMax = max(this.data, (d) => +d[this.radius]);

      // Create a scale based on data's min and max values
      roughnessScale = scaleLinear()
        .domain([roughnessMin, roughnessMax])
        .range([this.roughnessExtent[0], this.roughnessExtent[1]]);
    }

    this.initRoughObjects();

    const linkElements = this.svg
      .selectAll(".link")
      .data(this.links)
      .enter()
      .append("line")
      .attr("class", "link");

    const nodeGroups = this.svg
      .selectAll(".nodeGroup")
      .data(this.data)
      .enter()
      .append("g")
      .attr("class", "nodeGroup");

    nodeGroups.each(function (d, i) {
      const nodeRadius =
        typeof that.radius === "number"
          ? that.radius
          : radiusScale(d[that.radius]);

      const nodeRoughness =
        typeof that.roughness === "number"
          ? that.roughness
          : roughnessScale(d[that.roughness]);

      const node = that.rc.circle(0, 0, nodeRadius, {
        fill: that.colorCallback(d),
        simplification: that.simplification,
        fillWeight: that.fillWeight,
        roughness: nodeRoughness,
      });

      this.appendChild(node);

      node.setAttribute("class", that.graphClass + "_node");

      select(this)
        .append("circle")
        .attr("class", "node-circle")
        .attr("r", nodeRadius * 0.5)
        .attr("fill", "transparent")
        .attr("stroke-width", 0)
        .attr("stroke", "none");

      select(this)
        .append("text")
        .attr("class", "node-text")
        .attr("x", 0)
        .attr("y", -10) // Adjust 15 based on your needs
        .attr("text-anchor", "middle")
        .style("pointer-events", "none")
        .attr("stroke", "black")
        .attr("fill", "white")
        .attr("stroke-linejoin", "fill")
        .attr("paint-order", "stroke fill")
        .attr("stroke-width", "5px")
        .attr("opacity", 0)
        .text((d) => that.textCallback(d));
    });

    const simulation = forceSimulation(this.data);
    simulation.alpha(1).restart();

    simulation
      .force(
        "collide",
        forceCollide().radius((d) => d.radius * this.collision)
      )
      .force("center", forceCenter(this.width / 2, this.height / 2))
      .force("link", forceLink(this.links).distance(100));
    simulation.on("tick", () => {
      nodeGroups.attr("transform", (d) => `translate(${d.x}, ${d.y})`);
      linkElements
        .attr("x1", (d) => d.source.x)
        .attr("y1", (d) => d.source.y)
        .attr("x2", (d) => d.target.x)
        .attr("y2", (d) => d.target.y);
    });

    selectAll(".nodeGroup")
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);

    if (this.interactive === true) {
      this.addInteraction();
    }

    if (this.legend) {
      const legendItems = this.legend;
      this.colors = this.legend.map((item) => item.color);

      const legendWidth =
        legendItems.reduce(
          (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
          0
        ) *
          6 +
        35;
      const legendHeight = legendItems.length * 11 + 8;

      addLegend(this, legendItems, legendWidth, legendHeight);
    }
  }
}

export default Network;


================================================
FILE: src/Pie.js
================================================
import { csv, tsv, json } from "d3-fetch";
import { mouse, select, selectAll } from "d3-selection";
import { arc, pie } from "d3-shape";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { colors } from "./utils/colors";
import { addLegend } from "./utils/addLegend";
import { roughCeiling } from "./utils/roughCeiling";

/**
 * Pie chart class, which extends the Chart class.
 */
class Pie extends Chart {
  /**
   * Constructs a new Pie instance.
   * @param {Object} opts - Configuration object for the pie chart.
   */
  constructor(opts) {
    super(opts);
    // load in arguments from config object
    this.data = opts.data;
    this.margin = opts.margin || { top: 50, right: 20, bottom: 10, left: 20 };
    this.colors = opts.colors || colors;
    this.highlight = opts.highlight;
    this.roughness = roughCeiling({
      roughness: opts.roughness,
      ceiling: 30,
      defaultValue: 0,
    });
    this.strokeWidth = opts.strokeWidth || 0.75;
    this.innerStrokeWidth = opts.innerStrokeWidth || 0.75;
    this.fillWeight = opts.fillWeight || 0.85;
    this.labels = this.dataFormat === "object" ? "labels" : opts.labels;
    this.values = this.dataFormat === "object" ? "values" : opts.values;
    if (this.labels === undefined || this.values === undefined) {
      console.log(`Error for ${this.el}: Must include labels and values when \
       instantiating Donut chart. Skipping chart.`);
      return;
    }
    this.legend = opts.legend !== false;
    this.legendPosition = opts.legendPosition || "right";
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.radius = Math.min(this.width, this.height) / 2;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data) {
    // if data from file, read in
    // else if data from json object, read in
    if (typeof data === "string") {
      if (data.includes(".csv")) {
        return () => {
          csv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".tsv")) {
        return () => {
          tsv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".json")) {
        return () => {
          json(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      }
    } else {
      return () => {
        this.data = data;
        this.drawFromObject();
      };
    }
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 3)
      .attr("class", "title")
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(40, Math.min(this.width, this.height) / 4)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    selectAll(this.interactionG)
      .append("g")
      .attr("transform", `translate(${this.width / 2}, ${this.height / 2})`)
      .data(
        this.dataFormat === "object"
          ? this.makePie(this.data[this.values])
          : this.makePie(this.data)
      )
      .append("path")
      .attr("d", this.makeArc)
      .attr("stroke-width", "0px")
      .attr("fill", "transparent");

    // create tooltip
    const Tooltip = select(this.el)
      .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("position", "absolute")
      .style("background-color", "white")
      .style("border", "solid")
      .style("border-width", "1px")
      .style("border-radius", "5px")
      .style("padding", "3px")
      .style("font-family", this.fontFamily)
      .style("font-size", this.tooltipFontSize)
      .style("pointer-events", "none");

    // event functions
    let mouseover = function (d) {
      Tooltip.style("opacity", 1);
    };

    const that = this;
    let thisColor;

    let mousemove = function (d) {
      const attrX = select(this).attr("attrX");
      const attrY = select(this).attr("attrY");
      const mousePos = mouse(this);
      // get size of enclosing div
      Tooltip.html(`<b>${attrX}</b>: ${attrY}`)
        .style("opacity", 0.95)
        .style(
          "transform",
          `translate(${mousePos[0] + that.margin.left}px, 
              ${
                mousePos[1] -
                (that.height + that.margin.top + that.margin.bottom / 2)
              }px)`
        );
    };
    let mouseleave = function (d) {
      Tooltip.style("opacity", 0);
    };

    // d3 event handlers
    selectAll(this.interactionG).on("mouseover", function () {
      mouseover();
      thisColor = select(this).selectAll("path").style("stroke");
      that.highlight === undefined
        ? select(this).selectAll("path").style("opacity", 0.5)
        : select(this).selectAll("path").style("stroke", that.highlight);
    });

    selectAll(this.interactionG).on("mouseout", function () {
      mouseleave();
      select(this).selectAll("path").style("stroke", thisColor);
      select(this).selectAll("path").style("opacity", 1);
    });

    selectAll(this.interactionG).on("mousemove", mousemove);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: { strokeWidth: this.strokeWidth >= 3 ? 3 : this.strokeWidth },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        fill: this.color,
        strokeWidth: this.innerStrokeWidth,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    this.initRoughObjects();
    this.makePie = pie();

    this.makeArc = arc().innerRadius(0).outerRadius(this.radius);

    this.arcs = this.makePie(this.data[this.values]);
    this.arcs.forEach((d, i) => {
      if (d.value !== 0) {
        const node = this.rc.arc(
          this.width / 2, // x
          this.height / 2, // y
          2 * this.radius, // width
          2 * this.radius, // height
          d.startAngle - Math.PI / 2, // start
          d.endAngle - Math.PI / 2, // stop
          true,
          {
            fill: this.colors[i],
            stroke: this.colors[i],
          }
        );
        node.setAttribute("class", this.graphClass);
        const roughNode = this.roughSvg.appendChild(node);
        roughNode.setAttribute("attrY", this.data[this.values][i]);
        roughNode.setAttribute("attrX", this.data[this.labels][i]);
      }
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);

    const dataSources = this.data.labels;
    // ADD LEGEND
    const legendItems = dataSources.map((key, i) => ({
      color: this.colors[i],
      text: key,
    }));
    // find length of longest text item
    const legendWidth =
      legendItems.reduce(
        (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
        0
      ) *
        6 +
      35;
    const legendHeight = legendItems.length * 11 + 8;

    if (this.legend === true) {
      addLegend(this, legendItems, legendWidth, legendHeight);
    }

    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  }

  /**
   * Draw chart from file.
   */
  drawFromFile() {
    this.initRoughObjects();

    this.makePie = pie()
      .value((d) => d[this.values])
      .sort(null);

    const valueArr = [];
    this.makeArc = arc().innerRadius(0).outerRadius(this.radius);

    this.arcs = this.makePie(this.data);

    this.arcs.forEach((d, i) => {
      if (d.value !== 0) {
        // let c = this.makeArc.centroid(d);
        const node = this.rc.arc(
          this.width / 2, // x
          this.height / 2, // y
          2 * this.radius, // width
          2 * this.radius, // height
          d.startAngle - Math.PI / 2, // start
          d.endAngle - Math.PI / 2, // stop
          true,
          {
            fill: this.colors[i],
            stroke: this.colors[i],
          }
        );
        node.setAttribute("class", this.graphClass);
        const roughNode = this.roughSvg.appendChild(node);
        roughNode.setAttribute("attrY", d.data[this.values]);
        roughNode.setAttribute("attrX", d.data[this.labels]);
      }
      valueArr.push(d.data[this.labels]);
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);

    // ADD LEGEND
    const dataSources = valueArr;
    const legendItems = dataSources.map((key, i) => ({
      color: this.colors[i],
      text: key,
    }));
    // find length of longest text item
    const legendWidth =
      legendItems.reduce(
        (pre, cur) => (pre > cur.text.length ? pre : cur.text.length),
        0
      ) *
        6 +
      35;
    const legendHeight = legendItems.length * 11 + 8;

    if (this.legend === true) {
      addLegend(this, legendItems, legendWidth, legendHeight);
    }

    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw
}

export default Pie;


================================================
FILE: src/Scatter.js
================================================
import { extent, min, max } from "d3-array";
import { axisBottom, axisLeft } from "d3-axis";
import { csv, tsv } from "d3-fetch";
import { format } from "d3-format";
import { scaleLinear, scaleOrdinal } from "d3-scale";
import { mouse, select, selectAll } from "d3-selection";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { roughCeiling } from "./utils/roughCeiling";

const defaultColors = [
  "pink",
  "skyblue",
  "coral",
  "gold",
  "teal",
  "darkgreen",
  "brown",
  "slateblue",
  "orange",
];

/**
 * Scatter chart class, which extends the Chart class.
 */
class Scatter extends Chart {
  /**
   * Constructs a new Scatter instance.
   * @param {Object} opts - Configuration object for the scatter chart.
   */
  constructor(opts) {
    super(opts);

    // load in arguments from config object
    // this.data = opts.data;
    this.margin = opts.margin || { top: 50, right: 20, bottom: 50, left: 100 };
    this.colorVar = opts.colorVar;
    this.roughness = roughCeiling({ roughness: opts.roughness });
    this.highlight = opts.highlight;
    this.highlightLabel = opts.highlightLabel || "xy";
    // this.radius = opts.radius || 8;
    this.radiusExtent = opts.radiusExtent || [5, 20];
    this.radius = opts.radius || 20;
    this.axisStrokeWidth = opts.axisStrokeWidth || 0.4;
    this.axisRoughness = opts.axisRoughness || 0.9;
    this.curbZero = opts.curbZero === true;
    this.innerStrokeWidth = opts.innerStrokeWidth || 1;
    this.stroke = opts.stroke || "black";
    this.fillWeight = opts.fillWeight || 0.85;
    this.colors = opts.colors || defaultColors;
    this.strokeWidth = opts.strokeWidth || 1;
    this.axisFontSize = opts.axisFontSize;
    this.x = this.dataFormat === "object" ? "x" : opts.x;
    this.y = this.dataFormat === "object" ? "y" : opts.y;
    this.xValueFormat = opts.xValueFormat;
    this.yValueFormat = opts.yValueFormat;
    this.xLabel = opts.xLabel || "";
    this.yLabel = opts.yLabel || "";
    this.labelFontSize = opts.labelFontSize || "1rem";
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    this.radiusScale;
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
    window.addEventListener("resize", this.resizeHandler.bind(this));
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    // if (opts.title !== "undefined") {
    //   this.setTitle(opts.title);
    // }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    this.colors = opts.colors || this.colors;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.setSvg();
  }

  // add this to abstract base
  resolveData(data) {
    if (typeof data === "string") {
      if (data.includes(".csv")) {
        return () => {
          csv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".tsv")) {
        return () => {
          tsv(data).then((d) => {
            this.data = d;
            this.drawFromFile();
          });
        };
      }
    } else {
      return () => {
        this.data = data;
        this.drawFromObject();
      };
    }
  }

  addScaleLine() {
    let dataExtent;
    if (this.dataFormat !== "file") {
      dataExtent = allDataExtent(this.data);
    } else {
      const extents = this.dataSources.map((key) =>
        extent(this.data, (d) => +d[key])
      );
      const dataMin = min(extents, (d) => d[0]);
      const dataMax = max(extents, (d) => d[1]);
      dataExtent = [dataMin, dataMax];
    }
    // get value domains and pad axes by 5%
    // if this.x is undefined, use index for x
    let xExtent;
    if (this.x === undefined) {
      // get length of longest array
      const keys = Object.keys(this.data);
      const lengths = keys.map((key) => this.data[key].length);
      const maxLen = max(lengths);
      // Need to make xScale, when this.x is given, ordinal.
      xExtent =
        this.dataFormat === "file" ? [0, this.data.length] : [0, maxLen];
    } else {
      xExtent = extent(this.x);
    }

    const yExtent = dataExtent;

    const yRange = yExtent[1] - yExtent[0];

    this.xScale =
      this.x === undefined
        ? scalePoint()
            .range([0, this.width])
            .domain([...Array(xExtent[1]).keys()])
        : scalePoint().range([0, this.width]).domain(this.x);

    this.yScale = scaleLinear()
      .range([this.height, 0])
      .domain([0, yExtent[1] + yRange * 0.05]);
  }

  addScales() {
    // get value domains and pad axes by 5%
    const xExtent =
      this.dataFormat === "file"
        ? extent(this.data, (d) => +d[this.x])
        : extent(this.data[this.x]);
    const xRange = xExtent[1] - xExtent[0];
    const yExtent =
      this.dataFormat === "file"
        ? extent(this.data, (d) => +d[this.y])
        : extent(this.data[this.y]);
    const yRange = yExtent[1] - yExtent[0];
    // todo: why use xRange?
    // todo: why use yRange?

    const colorExtent =
      this.dataFormat === "file"
        ? extent(this.data, (d) => d[this.colorVar])
        : [1, 1];

    if (this.dataFormat === "file") {
      const radiusExtent = extent(this.data, (d) => +d[this.radius]);
      const radiusMax = Math.min(this.width, this.height) / 2 / 2;
      this.radiusScale = scaleLinear()
        .range([8, radiusMax])
        .domain(radiusExtent);
    } else {
      this.radiusScale = scaleLinear()
        .domain([0, 20])
        .range([this.radiusExtent[0], this.radiusExtent[1]]);
    }

    // force zero baseline if all data is positive
    if (this.curbZero === true) {
      if (yExtent[0] > 0) {
        yExtent[0] = 0;
      }
      if (xExtent[0] > 0) {
        xExtent[0] = 0;
      }
    }

    this.xScale = scaleLinear()
      .range([0, this.width])
      .domain([xExtent[0] - xRange * 0.05, xExtent[1] + xRange * 0.05]);

    this.yScale = scaleLinear()
      .range([this.height, 0])
      .domain([yExtent[0] - yRange * 0.05, yExtent[1] + yRange * 0.05]);

    this.colorScale = scaleOrdinal().range(this.colors).domain(colorExtent);
  }

  /**
   * Create x and y labels for chart.
   */
  addLabels() {
    // xLabel
    if (this.xLabel !== "") {
      this.svg
        .append("text")
        .attr("x", this.width / 2)
        .attr("y", this.height + this.margin.bottom / 1.3)
        .attr("dx", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.xLabel);
    }
    // yLabel
    if (this.yLabel !== "") {
      this.svg
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - this.margin.left / 2)
        .attr("x", 0 - this.height / 2)
        .attr("dy", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.yLabel);
    }
  }

  /**
   * Create x and y axes for chart.
   */
  addAxes() {
    const xAxis = axisBottom(this.xScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.xValueFormat ? format(this.xValueFormat)(d) : d;
      });

    const yAxis = axisLeft(this.yScale)
      .tickSize(0)
      .tickFormat((d) => {
        return this.yValueFormat ? format(this.yValueFormat)(d) : d;
      });

    // x-axis
    this.svg
      .append("g")
      .attr("transform", "translate(0," + this.height + ")")
      .call(xAxis)
      .attr("class", `xAxis${this.graphClass}`)
      .selectAll("text")
      .attr("transform", "translate(-10, 0)rotate(-45)")
      .style("text-anchor", "end")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      );

    // y-axis
    this.svg
      .append("g")
      .call(yAxis)
      .attr("class", `yAxis${this.graphClass}`)
      .selectAll("text")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      );

    // hide original axes
    selectAll("path.domain").attr("stroke", "transparent");

    selectAll("g.tick").style("opacity", 1);
  }

  makeAxesRough(roughSvg, rcAxis) {
    const xAxisClass = `xAxis${this.graphClass}`;
    const yAxisClass = `yAxis${this.graphClass}`;
    const roughXAxisClass = `rough-${xAxisClass}`;
    const roughYAxisClass = `rough-${yAxisClass}`;

    select(`.${xAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughXAxis = rcAxis.path(pathD, {
          stroke: "black",
          fillStyle: "hachure",
        });
        roughXAxis.setAttribute("class", roughXAxisClass);
        roughSvg.appendChild(roughXAxis);
      });
    selectAll(`.${roughXAxisClass}`).attr(
      "transform",
      `translate(0, ${this.height})`
    );

    select(`.${yAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughYAxis = rcAxis.path(pathD, {
          stroke: "black",
          fillStyle: "hachure",
        });
        roughYAxis.setAttribute("class", roughYAxisClass);
        roughSvg.appendChild(roughYAxis);
      });
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 2)
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(20, Math.min(this.width, this.height) / 4)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    // const that = this;
    // add highlight helper dom nodes
    const circles = selectAll(this.interactionG)
      .data(this.dataFormat === "file" ? this.data : this.data.x)
      .append("circle")
      .attr("cx", (d, i) => {
        // return 5;
        return this.dataFormat === "file"
          ? this.xScale(+d[this.x])
          : this.xScale(+this.data[this.x][i]);
      })
      .attr("cy", (d, i) => {
        return this.dataFormat === "file"
          ? this.yScale(+d[this.y])
          : this.yScale(+this.data[this.y][i]);
      });

    if (this.dataFormat === "file") {
      circles
        .attr("r", (d) =>
          typeof this.radius === "number"
            ? this.radius * 0.7
            : this.radiusScale(+d[this.radius]) * 0.6
        )
        .attr("fill", "transparent");
    } else {
      circles
        .attr("r", (d, i) => {
          const nodeRadius = this.data[this.radius][i];
          return typeof this.radius === "number"
            ? this.radius * 0.7
            : this.radiusScale(nodeRadius);
        })
        .attr("fill", "transparent");
    }

    // create tooltip
    let Tooltip = select(this.el)
      .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("position", "absolute")
      .style("background-color", "white")
      .style("border", "solid")
      .style("border-width", "1px")
      .style("border-radius", "5px")
      .style("padding", "3px")
      .style("font-family", this.fontFamily)
      .style("font-size", this.tooltipFontSize)
      .style("pointer-events", "none");

    // event functions
    let mouseover = function (d) {
      Tooltip.style("opacity", 1);
    };

    const that = this;
    let thisColor;

    let mousemove = function (d) {
      const attrX = select(this).attr("attrX");
      const attrY = select(this).attr("attrY");
      const attrHighlightLabel = select(this).attr("attrHighlightLabel");
      const mousePos = mouse(this);
      // get size of enclosing div
      Tooltip.html(
        that.highlightLabel === "xy"
          ? `<b>x</b>: ${attrX} <br><b>y</b>: ${attrY}`
          : `<b>${attrHighlightLabel}</b>`
      )
        .attr("class", function (d) {})
        .style(
          "transform",
          `translate(${mousePos[0] + that.margin.left}px, 
          ${
            mousePos[1] -
            (that.height + that.margin.top + that.margin.bottom / 2)
          }px)`
        );
    };
    let mouseleave = function (d) {
      Tooltip.style("opacity", 0);
    };

    // d3 event handlers
    selectAll(this.interactionG).on("mouseover", function () {
      mouseover();
      thisColor = select(this).selectAll("path").style("stroke");
      that.highlight === undefined
        ? select(this).selectAll("path:nth-child(1)").style("opacity", 0.4)
        : select(this)
            .selectAll("path:nth-child(1)")
            .style("stroke", that.highlight);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth + 1.2);
    });

    selectAll(this.interactionG).on("mouseout", function () {
      mouseleave();
      select(this).selectAll("path").style("opacity", 1);

      select(this).selectAll("path:nth-child(1)").style("stroke", thisColor);
      // highlight stroke back to its color
      select(this).selectAll("path:nth-child(2)").style("stroke", that.stroke);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth);
    });

    selectAll(this.interactionG).on("mousemove", mousemove);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: {
        strokeWidth: this.axisStrokeWidth,
        roughness: this.axisRoughness,
      },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        // fill: this.color,
        stroke: this.stroke === "none" ? undefined : this.stroke,
        strokeWidth: this.innerStrokeWidth,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    const that = this;
    this.radiusScale = scaleLinear()
      .domain([0, 20])
      .range([this.radiusExtent[0], this.radiusExtent[1]]);

    let radiusScale;
    let roughnessScale;

    if (typeof this.radius === "number") {
      radiusScale = scaleLinear()
        .domain([0, 1])
        .range([this.radiusExtent[0], this.radiusExtent[1]]);
    } else {
      const dataMin = min(this.data[this.radius]);
      const dataMax = max(this.data[this.radius]);
      // Create a scale based on data's min and max values
      radiusScale = scaleLinear()
        .domain([dataMin, dataMax])
        .range([this.radiusExtent[0], this.radiusExtent[1]]);
    }

    // set default color
    if (typeof this.colors === "string") this.colors = this.colors;
    if (this.colors === undefined) this.colors = defaultColors[0];

    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();

    // Add scatterplot
    this.data.x.forEach((d, i) => {
      const nodeRadius =
        typeof that.radius === "number"
          ? that.radius
          : radiusScale(+this.data[that.radius][i]);
      const node = this.rc.circle(
        this.xScale(+d),
        this.yScale(+this.data[this.y][i]),
        nodeRadius,
        {
          fill:
            typeof this.colors === "string"
              ? this.colors
              : this.colors.length === 1
              ? this.colors[0]
              : this.colors[i],
          simplification: this.simplification,
          fillWeight: this.fillWeight,
        }
      );
      const roughNode = this.roughSvg.appendChild(node);
      roughNode.setAttribute("class", this.graphClass);
      roughNode.setAttribute("attrX", d);
      roughNode.setAttribute("attrY", this.data[this.y][i]);
      roughNode.setAttribute(
        "attrHighlightLabel",
        this.data[this.highlightLabel]
      );
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  }

  /**
   * Draw chart from file.
   */
  drawFromFile() {
    // set default colors
    if (this.colors === undefined) this.colors = defaultColors;

    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();

    // Add scatterplot
    this.data.forEach((d, i) => {
      const node = this.rc.circle(
        this.xScale(+d[this.x]),
        this.yScale(+d[this.y]),
        typeof this.radius === "number"
          ? this.radius
          : this.radiusScale(+d[this.radius]),
        {
          fill:
            this.colorVar === undefined
              ? this.colors[0]
              : this.colorScale(d[this.colorVar]),
          simplification: this.simplification,
          fillWeight: this.fillWeight,
        }
      );
      const roughNode = this.roughSvg.appendChild(node);
      roughNode.setAttribute("class", this.graphClass);
      roughNode.setAttribute("attrX", d[this.x]);
      roughNode.setAttribute("attrY", d[this.y]);
      roughNode.setAttribute("attrHighlightLabel", d[this.highlightLabel]);
    });

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  }
}

export default Scatter;


================================================
FILE: src/StackedBar.js
================================================
import { max } from "d3-array";
import { axisBottom, axisLeft } from "d3-axis";
import { csv, tsv } from "d3-fetch";
import { scaleBand, scaleLinear, scaleOrdinal } from "d3-scale";
import { mouse, select, selectAll } from "d3-selection";
import rough from "roughjs/bundled/rough.esm.js";
import Chart from "./Chart";
import { colors } from "./utils/colors";
import { roughCeiling } from "./utils/roughCeiling";

/**
 * StackedBar chart class, which extends the Chart class.
 */
class StackedBar extends Chart {
  /**
   * Constructs a new StackedBar instance.
   * @param {Object} opts - Configuration object for the stacked bar chart.
   */
  constructor(opts) {
    super(opts);

    // load in arguments from config object
    this.data = opts.data;
    this.margin = opts.margin || { top: 50, right: 20, bottom: 70, left: 100 };
    this.color = opts.color || "red";
    this.highlight = opts.highlight || "coral";
    this.roughness = roughCeiling({ roughness: opts.roughness });
    this.stroke = opts.stroke || "black";
    this.strokeWidth = opts.strokeWidth || 1;
    this.axisStrokeWidth = opts.axisStrokeWidth || 0.5;
    this.axisRoughness = opts.axisRoughness || 0.5;
    this.innerStrokeWidth = opts.innerStrokeWidth || 1;
    this.fillWeight = opts.fillWeight || 0.5;
    this.axisFontSize = opts.axisFontSize;
    this.labels = opts.labels;
    this.values = opts.values;
    this.stackColorMapping = {};
    this.padding = opts.padding || 0.1;
    this.xLabel = opts.xLabel || "";
    this.yLabel = opts.yLabel || "";
    this.labelFontSize = opts.labelFontSize || "1rem";
    this.responsive = true;
    this.boundRedraw = this.redraw.bind(this, opts);
    // new width
    this.initChartValues(opts);
    // resolve font
    this.resolveFont();
    // create the chart
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();
    if (opts.title !== "undefined") this.setTitle(opts.title);
    window.addEventListener("resize", this.resizeHandler.bind(this));
  }

  /**
   * Handles window resize to redraw chart if responsive.
   */
  resizeHandler() {
    if (this.responsive) {
      this.boundRedraw();
    }
  }

  /**
   * Removes SVG elements and tooltips associated with the chart.
   */
  remove() {
    select(this.el).select("svg").remove();
  }

  /**
   * Redraws the bar chart with updated options.
   * @param {Object} opts - Updated configuration object for the bar chart.
   */
  redraw(opts) {
    // 1. Remove the current SVG associated with the chart.
    this.remove();

    // 2. Recalculate the size of the container.
    this.initChartValues(opts);

    // 3. Redraw everything.
    this.resolveFont();
    this.drawChart = this.resolveData(opts.data);
    this.drawChart();

    if (opts.title !== "undefined") {
      this.setTitle(opts.title);
    }
  }

  /**
   * Initialize the chart with default attributes.
   * @param {Object} opts - Configuration object for the chart.
   */
  initChartValues(opts) {
    this.roughness = opts.roughness || this.roughness;
    this.stroke = opts.stroke || this.stroke;
    this.strokeWidth = opts.strokeWidth || this.strokeWidth;
    this.axisStrokeWidth = opts.axisStrokeWidth || this.axisStrokeWidth;
    this.axisRoughness = opts.axisRoughness || this.axisRoughness;
    this.innerStrokeWidth = opts.innerStrokeWidth || this.innerStrokeWidth;
    this.fillWeight = opts.fillWeight || this.fillWeight;
    this.fillStyle = opts.fillStyle || this.fillStyle;
    const divDimensions = select(this.el).node().getBoundingClientRect();
    const width = divDimensions.width;
    const height = divDimensions.height;
    this.width = width - this.margin.left - this.margin.right;
    this.height = height - this.margin.top - this.margin.bottom;
    this.roughId = this.el + "_svg";
    this.graphClass = this.el.substring(1, this.el.length);
    this.interactionG = "g." + this.graphClass;
    this.setSvg();
  }

  // Helper Method to get the Total Value of the Stack
  getTotal(d) {
    for (let x = 0; x < d.length; x++) {
      let t = 0;
      for (let i = 0; i < d.columns.length; ++i) {
        if (d.columns[i] !== this.labels) {
          t += d[x][d.columns[i]] = +d[x][d.columns[i]];
        }
      }
      d[x].total = t;
    }
    return d;
  }

  updateColorMapping(label) {
    if (!this.stackColorMapping[label]) {
      // If there isn't a color already mapped to the label then use the next color available
      this.stackColorMapping[label] =
        colors[Object.keys(this.stackColorMapping).length];
    }
  }

  // add this to abstract base
  resolveData(data) {
    if (typeof data === "string") {
      if (data.includes(".csv")) {
        return () => {
          csv(data).then((d) => {
            this.getTotal(d);
            this.data = d;
            this.drawFromFile();
          });
        };
      } else if (data.includes(".tsv")) {
        return () => {
          tsv(data).then((d) => {
            this.getTotal(d);
            this.data = d;
            this.drawFromFile();
          });
        };
      }
    } else {
      return () => {
        this.data = data;

        // reset total key (need in case resize)
        data = data.map((d) => {
          if (Object.keys(d).includes("total")) {
            d["total"] = 0;
          }
          return d;
        });

        for (let i = 0; i < data.length; ++i) {
          let t = 0;
          const keys = Object.keys(data[i]);
          keys.forEach((d) => {
            if (d !== this.labels && d !== "total") {
              // exclude "total" key from accumulating
              this.updateColorMapping(d);
              t += data[i][d];
            }
          });
          data[i].total = t;
        }

        this.drawFromObject();
      };
    }
  }

  addScales() {
    this.xScale = scaleBand()
      .rangeRound([0, this.width])
      .padding(this.padding)
      .domain(this.data.map((d) => d[this.labels]));

    this.yScale = scaleLinear()
      .rangeRound([this.height, 0])
      .domain([
        0,
        max(this.data, (d) => {
          return d.total;
        }),
      ])
      .nice();

    // set the colors
    const keys =
      this.dataFormat === "object"
        ? this.data.map((d) => d[this.labels])
        : this.data.columns;
    this.zScale = scaleOrdinal()
      .range([
        "#98abc5",
        "#8a89a6",
        "#7b6888",
        "#6b486b",
        "#a05d56",
        "#d0743c",
        "#ff8c00",
      ])
      .domain(keys);
  }

  /**
   * Create x and y labels for chart.
   */
  addLabels() {
    // xLabel
    if (this.xLabel !== "") {
      this.svg
        .append("text")
        .attr("x", this.width / 2)
        .attr("y", this.height + this.margin.bottom / 2)
        .attr("dx", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.xLabel);
    }
    // yLabel
    if (this.yLabel !== "") {
      this.svg
        .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - this.margin.left / 1.4)
        .attr("x", 0 - this.height / 2)
        .attr("dy", "1em")
        .attr("class", "labelText")
        .style("text-anchor", "middle")
        .style("font-family", this.fontFamily)
        .style("font-size", this.labelFontSize)
        .text(this.yLabel);
    }
  }

  /**
   * Create x and y axes for chart.
   */
  addAxes() {
    const xAxis = axisBottom(this.xScale).tickSize(0);

    // x-axis
    this.svg
      .append("g")
      .attr("transform", "translate(0," + this.height + ")")
      .call(xAxis)
      .attr("class", `xAxis${this.graphClass}`)
      .selectAll("text")
      .attr("transform", "translate(-10,0)rotate(-45)")
      .style("text-anchor", "end")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.8, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      )
      .style("opacity", 0.9);

    // y-axis
    const yAxis = axisLeft(this.yScale).tickSize(0);
    this.svg
      .append("g")
      .call(yAxis)
      .attr("class", `yAxis${this.graphClass}`)
      .selectAll("text")
      .style("font-family", this.fontFamily)
      .style(
        "font-size",
        this.axisFontSize === undefined
          ? `${Math.min(0.95, Math.min(this.width, this.height) / 140)}rem`
          : this.axisFontSize
      )
      .style("opacity", 0.9);

    // hide original axes
    selectAll("path.domain").attr("stroke", "transparent");
  }

  makeAxesRough(roughSvg, rcAxis) {
    const xAxisClass = `xAxis${this.graphClass}`;
    const yAxisClass = `yAxis${this.graphClass}`;
    const roughXAxisClass = `rough-${xAxisClass}`;
    const roughYAxisClass = `rough-${yAxisClass}`;

    select(`.${xAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughXAxis = rcAxis.path(pathD, {
          fillStyle: "hachure",
        });
        roughXAxis.setAttribute("class", roughXAxisClass);
        roughSvg.appendChild(roughXAxis);
      });
    selectAll(`.${roughXAxisClass}`).attr(
      "transform",
      `translate(0, ${this.height})`
    );

    select(`.${yAxisClass}`)
      .selectAll("path.domain")
      .each(function (d, i) {
        const pathD = select(this).node().getAttribute("d");
        const roughYAxis = rcAxis.path(pathD, {
          fillStyle: "hachure",
        });
        roughYAxis.setAttribute("class", roughYAxisClass);
        roughSvg.appendChild(roughYAxis);
      });
  }

  /**
   * Set the chart title with the given title.
   * @param {string} title - The title for the chart.
   */
  setTitle(title) {
    this.svg
      .append("text")
      .attr("x", this.width / 2)
      .attr("y", 0 - this.margin.top / 2)
      .attr("class", "title")
      .attr("text-anchor", "middle")
      .style(
        "font-size",
        this.titleFontSize === undefined
          ? `${Math.min(40, Math.min(this.width, this.height) / 5)}px`
          : this.titleFontSize
      )
      .style("font-family", this.fontFamily)
      .style("opacity", 0.8)
      .text(title);
  }

  /**
   * Add interaction elements to chart.
   */
  addInteraction() {
    selectAll(this.interactionG)
      // .data(this.data)
      // .append('rect')
      .each(function (d, i) {
        const attr = this["attributes"];
        select(this)
          .append("rect")
          .attr("x", attr["x"].value)
          .attr("y", attr["y"].value)
          .attr("width", attr["width"].value)
          .attr("height", attr["height"].value)
          .attr("fill", "transparent");
      });

    // create tooltip
    const Tooltip = select(this.el)
      .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("position", "absolute")
      .style("background-color", "white")
      .style("border", "solid")
      .style("border-width", "1px")
      .style("border-radius", "5px")
      .style("padding", "3px")
      .style("font-family", this.fontFamily)
      .style("font-size", this.tooltipFontSize)
      .style("pointer-events", "none");

    // event functions
    const mouseover = function (d) {
      Tooltip.style("opacity", 1);
    };
    const that = this;
    let thisColor;

    let mousemove = function (d) {
      const attrX = select(this).attr("attrX");
      const attrY = select(this).attr("attrY");
      const mousePos = mouse(this);
      // get size of enclosing div
      Tooltip.html(`<b>${attrX}</b>: ${attrY}`)
        .style("opacity", 0.95)
        .attr("class", function (d) {})
        .style(
          "transform",
          `translate(${mousePos[0] + that.margin.left}px, 
          ${
            mousePos[1] -
            (that.height + that.margin.top + that.margin.bottom / 2)
          }px)`
        );
    };
    const mouseleave = function (d) {
      Tooltip.style("opacity", 0);
    };

    // d3 event handlers
    selectAll(this.interactionG).on("mouseover", function () {
      mouseover();
      thisColor = select(this).selectAll("path").style("stroke");
      select(this).select("path").style("stroke", that.highlight);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth + 1.2);
    });

    selectAll(this.interactionG).on("mouseout", function () {
      mouseleave();
      select(this).select("path").style("stroke", thisColor);
      select(this)
        .selectAll("path:nth-child(2)")
        .style("stroke-width", that.strokeWidth);
    });

    selectAll(this.interactionG).on("mousemove", mousemove);
  }

  /**
   * Draw rough SVG elements on chart.
   */
  initRoughObjects() {
    this.roughSvg = document.getElementById(this.roughId);
    this.rcAxis = rough.svg(this.roughSvg, {
      options: {
        strokeWidth: this.axisStrokeWidth,
        roughness: this.axisRoughness,
      },
    });
    this.rc = rough.svg(this.roughSvg, {
      options: {
        // fill: this.color,
        stroke: this.stroke === "none" ? undefined : this.stroke,
        strokeWidth: this.innerStrokeWidth,
        roughness: this.roughness,
        bowing: this.bowing,
        fillStyle: this.fillStyle,
      },
    });
  }

  // Helper Method to create the Stack
  stacking() {
    // Add Stackedbarplot
    this.data.forEach((d) => {
      const keys = Object.keys(d);
      let yStack = 0;
      keys.forEach((yValue, i) => {
        if (i > 0 && yValue !== "total") {
          yStack += parseInt(d[yValue], 10);
          const x = this.xScale(d[this.labels]);
          const y = this.yScale(yStack);
          const width = this.xScale.bandwidth();
          const height = this.height - this.yScale(+d[yValue]);
          const node = this.rc.rectangle(x, y, width, height, {
            fill: this.stackColorMapping[yValue] || this.colors[i],
            stroke: this.stackColorMapping[yValue] || this.colors[i],
            simplification: this.simplification,
            fillWeight: this.fillWeight,
          });
          const roughNode = this.roughSvg.appendChild(node);
          roughNode.setAttribute("class", this.graphClass);
          roughNode.setAttribute("attrX", d[this.labels]);
          roughNode.setAttribute("keyY", yValue);
          roughNode.setAttribute("attrY", +d[yValue]);
          // Set Attributes to access them later
          roughNode.setAttribute("x", x);
          roughNode.setAttribute("y", y);
          roughNode.setAttribute("width", width);
          roughNode.setAttribute("height", height);
        }
      });
    });
  }

  /**
   * Draw chart from object input.
   */
  drawFromObject() {
    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();
    // Add Stackedbarplot
    this.stacking();

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw

  /**
   * Draw chart from file.
   */
  drawFromFile() {
    this.initRoughObjects();
    this.addScales();
    this.addAxes();
    this.makeAxesRough(this.roughSvg, this.rcAxis);
    this.addLabels();
    // Add Stackedbar
    this.stacking();

    selectAll(this.interactionG)
      .selectAll("path:nth-child(2)")
      .style("stroke-width", this.strokeWidth);
    // If desired, add interactivity
    if (this.interactive === true) {
      this.addInteraction();
    }
  } // draw
}

export default StackedBar;


================================================
FILE: src/index.html
================================================
<!DOCTYPE html>
<html>
<head>
	<title>Parcel</title>
</head>
<body>
	<script src="./index.js"></script>
	<h1>main index</h1>
</body>
</html>

================================================
FILE: src/index.js
================================================
import Bar from "./Bar";
import BarH from "./BarH";
import Donut from "./Donut";
import Line from "./Line";
import Network from "./Network";
import Force from "./Force";
import Pie from "./Pie";
import Scatter from "./Scatter";
import StackedBar from "./StackedBar";

export { Bar, BarH, Donut, Line, Network, Force, Pie, Scatter, StackedBar };


================================================
FILE: src/utils/addFonts.js
================================================
/* eslint-disable max-len*/
export const addFontGaegu = (parent) => {
  parent.append("defs").append("style").attr("type", "text/css")
    .text(`@font-face {
    font-family: 'gaeguregular';
    src: url(data:application/font-woff2;charset=utf-8;base64,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) format('woff2'),
         url(data:application/font-woff;charset=utf-8;base64,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
Download .txt
gitextract_k9obim54/

├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src/
│   ├── Bar.js
│   ├── BarH.js
│   ├── Chart.js
│   ├── Donut.js
│   ├── Force.js
│   ├── Line.js
│   ├── Network.js
│   ├── Pie.js
│   ├── Scatter.js
│   ├── StackedBar.js
│   ├── index.html
│   ├── index.js
│   └── utils/
│       ├── addFonts.js
│       ├── addLegend.js
│       ├── colors.js
│       ├── roughCeiling.js
│       └── saveToPng.js
├── tests/
│   ├── Bar.test.js
│   ├── BarH.test.js
│   ├── Donut.test.js
│   ├── Pie.test.js
│   ├── Scatter.test.js
│   └── utils/
│       └── roughCeiling.test.js
├── vite.config.js
└── website/
    ├── index.html
    ├── main.js
    ├── package.json
    ├── roughDemo.js
    ├── style.css
    └── vite.config.js
Download .txt
SYMBOL INDEX (145 symbols across 12 files)

FILE: src/Bar.js
  class Bar (line 14) | class Bar extends Chart {
    method constructor (line 19) | constructor(opts) {
    method resizeHandler (line 59) | resizeHandler() {
    method remove (line 68) | remove() {
    method redraw (line 77) | redraw(opts) {
    method initChartValues (line 98) | initChartValues(opts) {
    method resolveData (line 121) | resolveData(data) {
    method addScales (line 149) | addScales() {
    method addLabels (line 173) | addLabels() {
    method addAxes (line 206) | addAxes() {
    method makeAxesRough (line 256) | makeAxesRough(roughSvg, rcAxis) {
    method setTitle (line 293) | setTitle(title) {
    method addInteraction (line 314) | addInteraction() {
    method initRoughObjects (line 402) | initRoughObjects() {
    method drawFromObject (line 425) | drawFromObject() {
    method drawFromFile (line 462) | drawFromFile() {

FILE: src/BarH.js
  class BarH (line 14) | class BarH extends Chart {
    method constructor (line 19) | constructor(opts) {
    method resizeHandler (line 59) | resizeHandler() {
    method remove (line 68) | remove() {
    method redraw (line 76) | redraw(opts) {
    method initChartValues (line 97) | initChartValues(opts) {
    method resolveData (line 119) | resolveData(data) {
    method addScales (line 144) | addScales() {
    method addLabels (line 167) | addLabels() {
    method addAxes (line 200) | addAxes() {
    method makeAxesRough (line 250) | makeAxesRough(roughSvg, rcAxis) {
    method setTitle (line 289) | setTitle(title) {
    method addInteraction (line 310) | addInteraction() {
    method initRoughObjects (line 393) | initRoughObjects() {
    method drawFromObject (line 416) | drawFromObject() {
    method drawFromFile (line 452) | drawFromFile() {

FILE: src/Chart.js
  class Chart (line 7) | class Chart {
    method constructor (line 12) | constructor(opts) {
    method setSvg (line 26) | setSvg() {
    method resolveFont (line 42) | resolveFont() {

FILE: src/Donut.js
  class Donut (line 13) | class Donut extends Chart {
    method constructor (line 18) | constructor(opts) {
    method resizeHandler (line 55) | resizeHandler() {
    method remove (line 63) | remove() {
    method redraw (line 70) | redraw(opts) {
    method initChartValues (line 91) | initChartValues(opts) {
    method resolveData (line 113) | resolveData(data) {
    method setTitle (line 149) | setTitle(title) {
    method addInteraction (line 170) | addInteraction() {
    method initRoughObjects (line 248) | initRoughObjects() {
    method drawFromObject (line 268) | drawFromObject() {
    method drawFromFile (line 345) | drawFromFile() {

FILE: src/Force.js
  class Force (line 14) | class Force extends Chart {
    method constructor (line 19) | constructor(opts) {
    method resizeHandler (line 59) | resizeHandler() {
    method remove (line 68) | remove() {
    method redraw (line 76) | redraw(opts) {
    method initChartValues (line 97) | initChartValues(opts) {
    method resolveData (line 124) | resolveData(data) {
    method setTitle (line 135) | setTitle(title) {
    method addInteraction (line 156) | addInteraction() {
    method initRoughObjects (line 195) | initRoughObjects() {
    method drawFromObject (line 215) | drawFromObject() {

FILE: src/Line.js
  class Line (line 26) | class Line extends Chart {
    method constructor (line 31) | constructor(opts) {
    method resizeHandler (line 85) | resizeHandler() {
    method remove (line 94) | remove() {
    method redraw (line 102) | redraw(opts) {
    method initChartValues (line 123) | initChartValues(opts) {
    method resolveData (line 144) | resolveData(data) {
    method addScales (line 169) | addScales() {
    method addLabels (line 215) | addLabels() {
    method addAxes (line 248) | addAxes() {
    method makeAxesRough (line 298) | makeAxesRough(roughSvg, rcAxis) {
    method setTitle (line 337) | setTitle(title) {
    method addInteraction (line 357) | addInteraction() {
    method initRoughObjects (line 475) | initRoughObjects() {
    method drawFromObject (line 497) | drawFromObject() {
    method drawFromFile (line 566) | drawFromFile() {

FILE: src/Network.js
  class Network (line 20) | class Network extends Chart {
    method constructor (line 25) | constructor(opts) {
    method resizeHandler (line 67) | resizeHandler() {
    method remove (line 76) | remove() {
    method redraw (line 84) | redraw(opts) {
    method initChartValues (line 105) | initChartValues(opts) {
    method resolveData (line 131) | resolveData(data, links) {
    method setTitle (line 143) | setTitle(title) {
    method addInteraction (line 164) | addInteraction() {
    method initRoughObjects (line 202) | initRoughObjects() {
    method drawFromObject (line 222) | drawFromObject() {

FILE: src/Pie.js
  class Pie (line 13) | class Pie extends Chart {
    method constructor (line 18) | constructor(opts) {
    method resizeHandler (line 57) | resizeHandler() {
    method remove (line 66) | remove() {
    method redraw (line 74) | redraw(opts) {
    method initChartValues (line 95) | initChartValues(opts) {
    method resolveData (line 117) | resolveData(data) {
    method setTitle (line 155) | setTitle(title) {
    method addInteraction (line 176) | addInteraction() {
    method initRoughObjects (line 254) | initRoughObjects() {
    method drawFromObject (line 273) | drawFromObject() {
    method drawFromFile (line 335) | drawFromFile() {

FILE: src/Scatter.js
  class Scatter (line 26) | class Scatter extends Chart {
    method constructor (line 31) | constructor(opts) {
    method resizeHandler (line 77) | resizeHandler() {
    method remove (line 86) | remove() {
    method redraw (line 94) | redraw(opts) {
    method initChartValues (line 115) | initChartValues(opts) {
    method resolveData (line 137) | resolveData(data) {
    method addScaleLine (line 162) | addScaleLine() {
    method addScales (line 205) | addScales() {
    method addLabels (line 261) | addLabels() {
    method addAxes (line 294) | addAxes() {
    method makeAxesRough (line 344) | makeAxesRough(roughSvg, rcAxis) {
    method setTitle (line 383) | setTitle(title) {
    method addInteraction (line 403) | addInteraction() {
    method initRoughObjects (line 520) | initRoughObjects() {
    method drawFromObject (line 543) | drawFromObject() {
    method drawFromFile (line 618) | drawFromFile() {

FILE: src/StackedBar.js
  class StackedBar (line 14) | class StackedBar extends Chart {
    method constructor (line 19) | constructor(opts) {
    method resizeHandler (line 58) | resizeHandler() {
    method remove (line 67) | remove() {
    method redraw (line 75) | redraw(opts) {
    method initChartValues (line 96) | initChartValues(opts) {
    method getTotal (line 117) | getTotal(d) {
    method updateColorMapping (line 130) | updateColorMapping(label) {
    method resolveData (line 139) | resolveData(data) {
    method addScales (line 188) | addScales() {
    method addLabels (line 225) | addLabels() {
    method addAxes (line 258) | addAxes() {
    method makeAxesRough (line 299) | makeAxesRough(roughSvg, rcAxis) {
    method setTitle (line 336) | setTitle(title) {
    method addInteraction (line 357) | addInteraction() {
    method initRoughObjects (line 439) | initRoughObjects() {
    method stacking (line 460) | stacking() {
    method drawFromObject (line 496) | drawFromObject() {
    method drawFromFile (line 517) | drawFromFile() {

FILE: src/utils/roughCeiling.js
  constant DEFAULT_CEILING (line 1) | const DEFAULT_CEILING = 20;
  constant DEFAULT_VALUE (line 2) | const DEFAULT_VALUE = 1;

FILE: website/roughDemo.js
  function createNodes (line 59) | function createNodes(numNodes) {
  function createLinks (line 78) | function createLinks(numNodes) {
  function getUpdatedValues (line 96) | function getUpdatedValues() {
  function resolveControls (line 131) | function resolveControls() {
  function newChart (line 169) | function newChart() {
  function updateChart (line 352) | function updateChart() {
  function handleDropdownChange (line 533) | function handleDropdownChange(event) {
  function getCurrentFillStyle (line 543) | function getCurrentFillStyle() {
  function updateSliderLabels (line 552) | function updateSliderLabels() {
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (388K chars).
[
  {
    "path": ".gitignore",
    "chars": 87,
    "preview": "dist/\nnode_modules\njest.config.js\nexamples/\n.cache\n.eslintrc\n.vscode/\npackage-lock.json"
  },
  {
    "path": "LICENSE",
    "chars": 1056,
    "preview": "Copyright (c) 2020 Jared Wilber\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this so"
  },
  {
    "path": "README.md",
    "chars": 21888,
    "preview": "<img src=\"https://raw.githubusercontent.com/jwilber/random_data/master/roughViz_Title.png\"  width=\"350\" alt=\"roughViz.js"
  },
  {
    "path": "package.json",
    "chars": 1272,
    "preview": "{\n  \"name\": \"rough-viz\",\n  \"version\": \"2.0.5\",\n  \"description\": \"Hand drawn, rough, sketchy data visualization in svg.\","
  },
  {
    "path": "src/Bar.js",
    "chars": 14720,
    "preview": "import { max } from \"d3-array\";\nimport { axisBottom, axisLeft } from \"d3-axis\";\nimport { csv, tsv } from \"d3-fetch\";\nimp"
  },
  {
    "path": "src/BarH.js",
    "chars": 14390,
    "preview": "import { max } from \"d3-array\";\nimport { axisBottom, axisLeft } from \"d3-axis\";\nimport { csv, tsv } from \"d3-fetch\";\nimp"
  },
  {
    "path": "src/Chart.js",
    "chars": 1675,
    "preview": "import { select } from \"d3-selection\";\nimport { addFontGaegu, addFontIndieFlower } from \"./utils/addFonts\";\n\n/**\n * Char"
  },
  {
    "path": "src/Donut.js",
    "chars": 12251,
    "preview": "import { csv, tsv, json } from \"d3-fetch\";\nimport { mouse, select, selectAll } from \"d3-selection\";\nimport { arc, pie } "
  },
  {
    "path": "src/Force.js",
    "chars": 10627,
    "preview": "import { mouse, select, selectAll } from \"d3-selection\";\nimport rough from \"roughjs/bundled/rough.esm.js\";\nimport Chart "
  },
  {
    "path": "src/Line.js",
    "chars": 18803,
    "preview": "import { bisect, extent, max, min, range } from \"d3-array\";\nimport { axisBottom, axisLeft } from \"d3-axis\";\nimport { csv"
  },
  {
    "path": "src/Network.js",
    "chars": 11038,
    "preview": "import { csv, tsv, json } from \"d3-fetch\";\nimport { mouse, select, selectAll } from \"d3-selection\";\nimport rough from \"r"
  },
  {
    "path": "src/Pie.js",
    "chars": 11755,
    "preview": "import { csv, tsv, json } from \"d3-fetch\";\nimport { mouse, select, selectAll } from \"d3-selection\";\nimport { arc, pie } "
  },
  {
    "path": "src/Scatter.js",
    "chars": 19840,
    "preview": "import { extent, min, max } from \"d3-array\";\nimport { axisBottom, axisLeft } from \"d3-axis\";\nimport { csv, tsv } from \"d"
  },
  {
    "path": "src/StackedBar.js",
    "chars": 15729,
    "preview": "import { max } from \"d3-array\";\nimport { axisBottom, axisLeft } from \"d3-axis\";\nimport { csv, tsv } from \"d3-fetch\";\nimp"
  },
  {
    "path": "src/index.html",
    "chars": 140,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n\t<title>Parcel</title>\n</head>\n<body>\n\t<script src=\"./index.js\"></script>\n\t<h1>main index<"
  },
  {
    "path": "src/index.js",
    "chars": 345,
    "preview": "import Bar from \"./Bar\";\nimport BarH from \"./BarH\";\nimport Donut from \"./Donut\";\nimport Line from \"./Line\";\nimport Netwo"
  },
  {
    "path": "src/utils/addFonts.js",
    "chars": 183185,
    "preview": "/* eslint-disable max-len*/\nexport const addFontGaegu = (parent) => {\n  parent.append(\"defs\").append(\"style\").attr(\"type"
  },
  {
    "path": "src/utils/addLegend.js",
    "chars": 1914,
    "preview": "import { select } from \"d3-selection\";\n\nexport const addLegend = (\n  parent,\n  legendItems,\n  legendWidth,\n  legendHeigh"
  },
  {
    "path": "src/utils/colors.js",
    "chars": 170,
    "preview": "export const colors = [\n  \"coral\",\n  \"skyblue\",\n  \"#66c2a5\",\n  \"tan\",\n  \"#8da0cb\",\n  \"#e78ac3\",\n  \"#a6d854\",\n  \"#ffd92f\""
  },
  {
    "path": "src/utils/roughCeiling.js",
    "chars": 326,
    "preview": "export const DEFAULT_CEILING = 20;\nexport const DEFAULT_VALUE = 1;\n\nexport const roughCeiling = ({\n  roughness,\n  ceilin"
  },
  {
    "path": "src/utils/saveToPng.js",
    "chars": 1110,
    "preview": "export saveToPng(filename = \"chart.png\") {\n    // 1. Serialize the SVG to a string\n    const svgString = new XMLSerializ"
  },
  {
    "path": "tests/Bar.test.js",
    "chars": 302,
    "preview": "// import roughBars from '../src';\n\ndescribe('Test Chart', () => {\n  test('Attributes should correctly propagate', () =>"
  },
  {
    "path": "tests/BarH.test.js",
    "chars": 357,
    "preview": "// import roughBars from '../src';\n\ndescribe('Filter function', () => {\n  test('it should filter by a search term (link)"
  },
  {
    "path": "tests/Donut.test.js",
    "chars": 302,
    "preview": "// import roughBars from '../src';\n\ndescribe('Test Chart', () => {\n  test('Attributes should correctly propagate', () =>"
  },
  {
    "path": "tests/Pie.test.js",
    "chars": 302,
    "preview": "// import roughBars from '../src';\n\ndescribe('Test Chart', () => {\n  test('Attributes should correctly propagate', () =>"
  },
  {
    "path": "tests/Scatter.test.js",
    "chars": 302,
    "preview": "// import roughBars from '../src';\n\ndescribe('Test Chart', () => {\n  test('Attributes should correctly propagate', () =>"
  },
  {
    "path": "tests/utils/roughCeiling.test.js",
    "chars": 1626,
    "preview": "import { roughCeiling, DEFAULT_CEILING, DEFAULT_VALUE } from '../../src/utils/roughCeiling';\n\ndescribe('roughCeiling', ("
  },
  {
    "path": "vite.config.js",
    "chars": 397,
    "preview": "import { defineConfig } from \"vite\";\nimport { terser } from \"rollup-plugin-terser\";\n\nexport default defineConfig({\n  bui"
  },
  {
    "path": "website/index.html",
    "chars": 10087,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>roughViz.js</title>\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href="
  },
  {
    "path": "website/main.js",
    "chars": 349,
    "preview": "import \"./style.css\";\nimport { Bar } from \"rough-viz\";\n\n// logo\nnew Bar({\n  element: \"#vizLogo\", // container selection\n"
  },
  {
    "path": "website/package.json",
    "chars": 382,
    "preview": "{\n  \"name\": \"rvtest\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n   "
  },
  {
    "path": "website/roughDemo.js",
    "chars": 17652,
    "preview": "import {\n  Bar,\n  BarH,\n  Donut,\n  Line,\n  Network,\n  Force,\n  Pie,\n  Scatter,\n  StackedBar,\n} from \"rough-viz\";\nimport "
  },
  {
    "path": "website/style.css",
    "chars": 1944,
    "preview": "@import url(\"https://fonts.googleapis.com/css?family=Gaegu\");\n\nmain {\n  margin: auto;\n  max-width: 1400px;\n}\n\n#viz0, #vi"
  },
  {
    "path": "website/vite.config.js",
    "chars": 192,
    "preview": "import { defineConfig } from \"vite\";\nimport { svelte } from \"@sveltejs/vite-plugin-svelte\";\n\n// https://vitejs.dev/confi"
  }
]

About this extraction

This page contains the full source code of the jwilber/roughViz GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (367.7 KB), approximately 176.1k tokens, and a symbol index with 145 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!