Skip to content

Commit a819efb

Browse files
committed
add 'sketchmode' layout property and logic for implementation
1 parent 4f7a513 commit a819efb

File tree

3 files changed

+89
-0
lines changed

3 files changed

+89
-0
lines changed

src/plot_api/plot_api.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
var d3 = require('@plotly/d3');
44
var isNumeric = require('fast-isnumeric');
55
var hasHover = require('has-hover');
6+
var rough = require('roughjs');
67

78
var Lib = require('../lib');
89
var nestedProperty = Lib.nestedProperty;
@@ -385,6 +386,9 @@ function _doPlot(gd, data, layout, config) {
385386
Plots.previousPromises
386387
);
387388

389+
// Implement sketchmode here (needs to happen once everything is drawn)
390+
seq.push(sketchifyFunc(gd));
391+
388392
// even if everything we did was synchronous, return a promise
389393
// so that the caller doesn't care which route we took
390394
var plotDone = Lib.syncOrAsync(seq, gd);
@@ -396,6 +400,76 @@ function _doPlot(gd, data, layout, config) {
396400
});
397401
}
398402

403+
function pathIsClosed(path) {
404+
// A closed path ends with 'Z' or 'z' (closepath)
405+
if(!path) return false;
406+
return path.trim().slice(-1).toLowerCase() === 'z';
407+
}
408+
409+
function extractRoughPathString(roughPath) {
410+
const pathElements = d3.select(roughPath).selectAll('path')[0];
411+
const pathStrings = pathElements.map(p => p.getAttribute('d') || '');
412+
// Return concatenated pathStrings
413+
return pathStrings.join(' ');
414+
}
415+
416+
function sketchifyFunc(gd) {
417+
function sketchify() {
418+
// Get value of `sketchmode` from the layout
419+
var sketchmode = gd._fullLayout.sketchmode;
420+
if(!sketchmode) return;
421+
422+
// Get the root svg nodes
423+
const mainSvgs = d3.select(gd).selectAll('.main-svg')[0];
424+
425+
for(var mainSvg of mainSvgs) {
426+
let roughSvg = rough.svg(mainSvg);
427+
428+
// Traverse all paths in the mainSvg and replace them with rough paths
429+
d3.select(mainSvg).selectAll('path').each(function() {
430+
const path = d3.select(this);
431+
const d = path.attr('d');
432+
const isClosedPath = pathIsClosed(d);
433+
434+
const pathStyle = path.attr('style') || '';
435+
const pathFill = (pathStyle.match(/fill:\s*([^;]*)/) || [])[1];
436+
const pathStroke = (pathStyle.match(/stroke:\s*([^;]*)/) || [])[1];
437+
const fillStyle = isClosedPath ? 'hachure' : 'solid';
438+
439+
if(d) {
440+
const options = {
441+
stroke: (pathStroke !== 'none') ? pathStroke : pathFill,
442+
fill: (pathFill !== 'none') ? pathFill : pathStroke,
443+
fillStyle: fillStyle,
444+
fillWeight: 3,
445+
hachureAngle: -45,
446+
hachureGap: 6,
447+
}
448+
449+
const roughPath = roughSvg.path(d, options);
450+
451+
const roughPathString = extractRoughPathString(roughPath);
452+
453+
if(!roughPathString) return;
454+
455+
// replace 'd' attribute of original path with that of rough path
456+
const strokeWidth = 2;
457+
path.attr('d', roughPathString);
458+
459+
path.attr('style', undefined);
460+
path.attr('stroke-width', '1')
461+
path.attr('fill', 'none');
462+
if(pathFill || pathStroke) {
463+
const strokeToSet = (pathStroke && pathStroke !== 'none') ? pathStroke : pathFill;
464+
path.attr('stroke', strokeToSet);
465+
}
466+
}
467+
});
468+
}
469+
}
470+
return sketchify;
471+
}
472+
399473
function emitAfterPlot(gd) {
400474
var fullLayout = gd._fullLayout;
401475

@@ -1768,6 +1842,8 @@ function relayout(gd, astr, val) {
17681842
relayout, [gd, specs.redoit]
17691843
);
17701844

1845+
seq.push(sketchifyFunc(gd));
1846+
17711847
var plotDone = Lib.syncOrAsync(seq, gd);
17721848
if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);
17731849

@@ -2288,6 +2364,8 @@ function update(gd, traceUpdate, layoutUpdate, _traces) {
22882364
Plots.reselect
22892365
);
22902366

2367+
seq.push(sketchifyFunc(gd));
2368+
22912369
Queue.add(gd,
22922370
update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces],
22932371
update, [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces]

src/plots/layout_attributes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,4 +451,14 @@ module.exports = {
451451
].join(' '),
452452
editType: 'none'
453453
}),
454+
455+
sketchmode: {
456+
valType: 'boolean',
457+
dflt: false,
458+
editType: 'plot',
459+
description: [
460+
'Determines whether the plot should be rendered with sketch-style appearance.',
461+
'When set to `true`, plot elements will have a hand-drawn, sketchy look.'
462+
].join(' ')
463+
},
454464
};

src/plots/plots.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,7 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) {
14011401
coerce('paper_bgcolor');
14021402

14031403
coerce('separators', formatObj.decimal + formatObj.thousands);
1404+
coerce('sketchmode');
14041405
coerce('hidesources');
14051406

14061407
coerce('colorway');

0 commit comments

Comments
 (0)