Skip to content

Revamp of mode bar's createIcon #2762

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 5, 2018
Merged

Revamp of mode bar's createIcon #2762

merged 6 commits into from
Jul 5, 2018

Conversation

vdh
Copy link
Contributor

@vdh vdh commented Jun 28, 2018

As a fix for #1335, a backwards-compatible change to createIcon in modebar.js.

  1. If an existing icon config is given, it behaves the same as before.
  2. If an icon config defines height, it will be used instead of ascent and descent.
  3. If an icon config defines transform, it will be used instead of the default transform.
  4. If an icon config doesn't define either a transform or an ascent, it will not be transformed at all.
  5. As an extra bonus, if an icon config is a function it will be called instead of createIcon, in the case where someone wants to completely customise the icon element generation (i.e. inline their own SVGs, or another element).

The pull_font_svg.js util was also updated to regenerate a new ploticon.js, containing height and transform props instead of the previous ascent and descent props. Assuming that this icon data is only used for its intended purpose in the mode bar (i.e. not used/manipulated manually for anything else by users), this shouldn't cause any breakages.

Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice PR. Thanks very much!

We'll need to add a few tests in modebar_test.js before merging, let me know if you need help.

button.appendChild(this.createIcon(config.icon || Icons.question, config.name));
var icon = config.icon;
if(typeof icon === 'function') {
button.appendChild(icon());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would config.icon ever be a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it easier to manually create the SVG element and bypass createIcon altogether.

For example, I have a version of this patched into my own code, and I use the icon function to generate an SVG with two g group elements with two icon path elements, with the second one set to opacity: 0. I can then use CSS based around modebar-btn and the active class to toggle between the two icon paths when Plotly toggles the active state of the button.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We like to keep our API JSON-serializable, unless we have to (like modebar button click handlers), so I'd vote 👎

The way you implemented the use case you described seems a little hacky. Maybe instead we could add a field named e.g activeIcon that would be used when active class is turned on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@etpinard
This issue arose because the current custom icon support is extremely tailored and twisted to only support the structure that the Plotly icons have been stored in. I mean, you've got:

  • Forced transformation inversion of the entire image… just because…?
  • Limited to only one path element
  • The viewBox is pieced together from disparate config variables
  • ascent and descent is never documented anywhere, leaving poor users to guess at (or poke at source code) to even understand how this will change the resulting SVG.
  • The fact that the Javascript is manually building the SVGs piece by piece in the first place, instead of loading the SVGs in directly, or applying them via CSS…
  • A custom transformation being applied to one special icon, but not actually via the icon's own config… no… it's actually applied via the name of the button config that just so happens to use that icon…???

I've tried very hard with this PR to keep everything supported, smooth over some of those wrinkles, while also opening it up enough for sane custom icons… so why is this small function-based escape hatch so bad? Amongst a config already full of existing support for functions?

What if someone wants to use SVG sprite sheets? What if they just want to load in their own SVG source files without jumping through hoops? What if they want to use CSS-based icon sets, to load the images in via their existing asset systems? Or heaven forbid, use more than just one path element?! Arbitrarily forcing your users to be restricted to one little use case for their SVGs just to force a JSON structure is maddening! It's not even like I'm forcing this option either, the JSON structure option still works just fine!

I'm sorry to get so heated about this, but calling it "a little hacky" is pot calling the kettle black 😢

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha @vdh you raise some good points. FWIW most of the quirks you mention above are due to the structure of SVG fonts - for example https://stackoverflow.com/questions/14203365/why-svg-font-is-mirrored - because that's where we do our own design in the first place, and at least at one point we were also using these icons as a font, though I'm not sure we're still doing that anywhere... not that this justifies an obtuse API, just to explain how we got here.

You're right that config items don't have a hard requirement for JSON (unlike data and layout where we cannot allow functions, for portability). @etpinard has good reason to encourage JSON, as it makes cross-language integrations (Python, R, etc) much cleaner. And, pots and kettles notwithstanding, new API gets held to a higher standard than old locked-in API, #420 is bloated enough already 😉

But I have to say I can see the appeal of a more flexible arrangement, and I don't really see a good alternative here. Multiple paths is a good example, what if you want regions with different colors or something? The use cases you've mentioned could be covered by a string of raw HTML, but I don't think that's better, as it could open an XSS vector. So yeah, I think in this case a function may be our best option.

Copy link
Contributor

@etpinard etpinard Jul 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vdh sorry, I didn't mean to criticize your implementation by calling it hacky. Finding the right hack can be the smartest thing one can do in some situations. But we can't expect every plotly.js user to be come up with that hack. I was simply wondering if there was another (I won't call it less hacky, say more user-intermediate-user-friendly) solution to your use case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple paths is a good example, what if you want regions with different colors or something? The use cases you've mentioned could be covered by a string of raw HTML, but I don't think that's better, as it could open an XSS vector. So yeah, I think in this case a function may be our best option.

I can get behind this (at least as a solution for v1).

👌 for typeof icon === 'function'.

@@ -169,7 +175,9 @@ proto.createButton = function(config) {
* @Return {HTMLelement}
*/
proto.createIcon = function(thisIcon, name) {
var iconHeight = thisIcon.ascent - thisIcon.descent,
var iconHeight = thisIcon.height !== undefined ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be more strict here:

isNumeric(this.height) ? Number(thisIcon.height) : thisIcon.ascent - this.decent

@vdh
Copy link
Contributor Author

vdh commented Jun 29, 2018

@etpinard I've added some tests, how do they look?

@etpinard
Copy link
Contributor

I've added some tests, how do they look?

Your tests look amazing. Thanks very much!

// Legacy icon transform calculation
var transform = name === 'toggleSpikelines' ?
'matrix(1.5 0 0 -1.5 0 ' + thisIcon.ascent + ')' :
'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for moving pull_font_svg to the new transform property - it's definitely cleaner to calculate that upfront - and for maintaining the legacy version to support any users who did go to the trouble of figuring out ascent and descent. But I don't think we need to carry the toggleSpikelines hack over here - lets take it out, then we don't even need name in createIcon 🎉

now that this is baked into pull_font_svg
@alexcjohnson
Copy link
Collaborator

@etpinard @vdh I removed the legacy toggleSpikeLines hack as I mentioned #2762 (comment) -> 586ff25
If you're OK with this, I think we're ready to go! 💃

@etpinard
Copy link
Contributor

etpinard commented Jul 5, 2018

Thanks @alexcjohnson ! Merging this thing 💃

@etpinard etpinard merged commit 07fa382 into plotly:master Jul 5, 2018
@vdh
Copy link
Contributor Author

vdh commented Jul 6, 2018

@etpinard @alexcjohnson
One possible solution I could think of that would work well enough to negate most needs for the custom function would be to add options to:

  • open up the API more for CSS-based solutions, so that custom SVGs (or images) can be loaded in externally via CSS e.g. icon: { id: 'custom' } or icon: { className: 'fas fa-stroopwafel' } will create an empty span with the given id/class, or will add id/class onto the generated SVG if the usual path/width/etc options are also provided.
  • a config option for SVG sprites, like icon: { useHref: '#custom' } which would generate <svg><use href="#custom" /></svg>

@palimondo
Copy link

Heh, this broke my code temporarily, but the fix was just to replace my own iconHeight computation from ascent and descent with height and using the precomputed transform instead of building one myself from ascent… so I just wanted to say kudos! This is much saner serialization in the json alone (I haven't looked at the rest of changes...).

@vdh vdh deleted the create-icon-revamp branch May 30, 2019 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants