Skip to content

Commit

Permalink
A bunch of cleanups, more documentation, and elmination of flattenSeg…
Browse files Browse the repository at this point in the history
…ments
  • Loading branch information
cereallarceny committed Jul 18, 2019
1 parent 763db67 commit c33e9b8
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 200 deletions.
76 changes: 14 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ gp.render({

```js
import * as d3 from 'd3';
import { getData, strokeToFill, flattenSegments } from 'gradient-path';
import { getData, strokeToFill } from 'gradient-path';

const segments = 30,
samples = 3,
Expand All @@ -86,12 +86,8 @@ const colors = d3.interpolateRainbow;
// Make sure to remove() the node
const path = d3.select('path').remove();

const data = getData({
path: path.node(),
segments,
samples,
precision
});
const data = getData({ path, segments, samples, precision });
const flattenedData = data.flatMap(({ samples }) => samples);

const lineFunc = d3
.line()
Expand All @@ -108,7 +104,7 @@ d3.select('svg')

d3.select('svg')
.selectAll('circle')
.data(flattenSegments(data))
.data(flattenedData)
.enter()
.append('circle')
.attr('cx', d => d.x)
Expand Down Expand Up @@ -142,12 +138,7 @@ gp.render(...)
const path = d3.select('path').remove();

// Our Gradient Path data array
const data = getData({
path: path.node(),
segments,
samples,
precision
});
const data = getData({ path, segments, samples, precision });

// D3 code here...
```
Expand All @@ -156,7 +147,7 @@ const data = getData({

The four keys are `path`, `segments`, `samples`, and `precision`:

- **path** (_required_) - This is the `path` you intend to convert to a gradient path. **It must be a valid DOM node.**
- **path** (_required_) - This is the `path` you intend to convert to a gradient path. **It must be a valid DOM node or D3 selection.**

- **segments** (_required_) - The number of segments you want in your path or circles. You can also think of this as: "how many different colors do I want to display?"

Expand All @@ -166,6 +157,8 @@ The four keys are `path`, `segments`, `samples`, and `precision`:

### Render function

**This only applies to usage with plain Javascript, it doesn't work with D3.**

The `render()` function is the only function available in the `GradientPath` class. It is responsible for taking it's own configuration object consisting of a few properties:

```js
Expand Down Expand Up @@ -200,24 +193,15 @@ You may have as many `render()` functions as you desire. Here's the structure of

As a general rule, if you use `fill` then you'll need to define a `width`. Likewise, if you use a `stroke` then you will need to defined a `strokeWidth`. Defaults are not set for these values.

### Further usage with D3
### Outlining a stroke

A keen eye might have spotted a few extra functions for providing nice-to-have support in D3. Of course, you have the `getData()` function mentioned above which gives you data to work with. However, depending on what you're rendering and how you're rendering it, you'll need two other function at your disposal:
**This only applies to usage with D3.**

#### strokeToFill(data, width, precision)

Perhaps the coolest function of all. This turns any stroke data (the kind of data produced by `getData()`) into an outlined `path` capable of being filled. Simply put:

> If you're planning on using `fill` on any `path` under Gradient Path, you'll need to convert that data to be outlined. You cannot `fill` a `stroke`. You must outline your stroked data first.
A keen eye might have spotted a subtle difference in how we use `getData()` when filling an SVG `path`. In order to work with `fill`, you'll first need to outline your data. You may do this using the `strokeToFill(data, width, precision)` function. You can use it like such:

```js
// We got the data in stroke form
const data = getData({
path: path.node(),
segments,
samples,
precision
});
const data = getData({ path, segments, samples, precision });

// Time to outline that data!
const outlinedData = strokeToFill(data, width, precision);
Expand All @@ -228,48 +212,16 @@ const lineFunc = d3
.x(d => d.x)
.y(d => d.y);

// Fill it up!
// Run through our paths
d3.select('svg')
.selectAll('path')
.data(outlinedData);
.enter()
.append('path')
.attr('fill', d => colors(d.progress))
.attr('fill', d => colors(d.progress)) // Now we can fill...
.attr('d', d => lineFunc(d.samples));
```

#### flattenSegments(data)

Also cool, but not nearly as snazzy. This function is quite helpful when working with SVG `circle` shapes. Naturally, the extra layer of organizations that segments give us is helpful when working with _line segments_ (`path`'s), but not so helpful with `circle`'s. Simply put:

> In order to work with any SVG `circle` shapes, you'll need to flatten your line segments into just a big array of samples.
```js
// We got the data in stroke form
const data = getData({
path: path.node(),
segments,
samples,
precision
});

// Flatten dat data!
const flattenedData = flattenSegments(data);

// Draw some dots
d3.select('svg')
.selectAll('circle')
.data(flattenedData)
.enter()
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 1.5)
.attr('fill', '#eee')
.attr('stroke', '#444')
.attr('stroke-width', 0.5);
```

## Contributing

1. `yarn install` - installs all dev dependencies
Expand Down
50 changes: 27 additions & 23 deletions src/GradientPath.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,80 @@
import { getData, strokeToFill, flattenSegments } from './_data';
import { svgElem, styleAttrs, segmentToD } from './_utils';
import { getData, strokeToFill } from './_data';
import { svgElem, styleAttrs, segmentToD, convertPathToNode } from './_utils';

export const DEFAULT_PRECISION = 2;

export default class GradientPath {
constructor({ path, segments, samples, precision = DEFAULT_PRECISION }) {
// If the path being passed isn't a DOM node already, make it one
path =
path instanceof Element || path instanceof HTMLDocument
? path
: path.node();
this.path = convertPathToNode(path);

this.path = path;
this.segments = segments;
this.samples = samples;
this.precision = precision;

// Store the render cycles that the user creates
this.renders = [];

// Append a group to the SVG to capture everything we render and ensure our paths and circles are properly encapsulated
this.svg = path.closest('svg');
this.group = svgElem('g', {
class: 'gradient-path'
});

// Get the data
this.data = getData({ path, segments, samples, precision });

// Append the main group to the SVG
this.svg.appendChild(this.group);

// Remove the main path once we have the data values
this.path.parentNode.removeChild(path);
this.path.parentNode.removeChild(this.path);
}

render({ type, stroke, strokeWidth, fill, width }) {
const { group, precision } = this,
renderCycle = {};
// Store information from this render cycle
const renderCycle = {};

// Create a group for each element
const elemGroup = svgElem('g', { class: `element-${type}` });
group.appendChild(elemGroup);

this.group.appendChild(elemGroup);
renderCycle.group = elemGroup;

if (type === 'path') {
// If we specify a width, we will be filling, so we need to outline the path and then average the join points of the segments
// If we specify a width and fill, then we need to outline the path and then average the join points of the segments
// If we do not specify a width and fill, then we will be stroking and can leave the data "as is"
renderCycle.data =
width && fill ? strokeToFill(this.data, width, precision) : this.data;
width && fill
? strokeToFill(this.data, width, this.precision)
: this.data;

for (let j = 0; j < renderCycle.data.length; j++) {
const segment = renderCycle.data[j];
const { samples, progress } = renderCycle.data[j];

// Create a path for each segment (array of samples) and append it to its elemGroup
// Create a path for each segment and append it to its elemGroup
elemGroup.appendChild(
svgElem('path', {
class: 'path-segment',
d: segmentToD(segment.samples),
...styleAttrs(fill, stroke, strokeWidth, segment.progress)
d: segmentToD(samples),
...styleAttrs(fill, stroke, strokeWidth, progress)
})
);
}
} else if (type === 'circle') {
renderCycle.data = flattenSegments(this.data);
renderCycle.data = this.data.flatMap(({ samples }) => samples);

for (let j = 0; j < renderCycle.data.length; j++) {
const sample = renderCycle.data[j];
const { x, y, progress } = renderCycle.data[j];

// Create a circle for each sample (because we called "flattenSegments(data)" on the line before) and append it to its elemGroup
// Create a circle for each sample and append it to its elemGroup
elemGroup.appendChild(
svgElem('circle', {
class: 'circle-sample',
cx: sample.x,
cy: sample.y,
cx: x,
cy: y,
r: width / 2,
...styleAttrs(fill, stroke, strokeWidth, sample.progress)
...styleAttrs(fill, stroke, strokeWidth, progress)
})
);
}
Expand All @@ -80,6 +83,7 @@ export default class GradientPath {
// Save the information in the current renderCycle and pop it onto the renders array
this.renders.push(renderCycle);

// Return this for method chaining
return this;
}
}
Loading

0 comments on commit c33e9b8

Please sign in to comment.