Skip to content
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

Proposal for default styling using pseudo-classes. #1754

Closed
flowchartsman opened this issue Dec 2, 2023 · 8 comments
Closed

Proposal for default styling using pseudo-classes. #1754

flowchartsman opened this issue Dec 2, 2023 · 8 comments

Comments

@flowchartsman
Copy link

flowchartsman commented Dec 2, 2023

Despite the addition of glob semantics, default styling still doesn't really exist in D2. Currently there are two options to style multiple entities with globs, both of which have their drawbacks:

Given the following diagram:

classes: {
  decision: {
    shape: diamond
    style.fill: red
  }
  actor: {
    shape: person
  }
}
direction: right
u: {
  label: you
  class: actor
}
w: {
  label: which?
  class: decision
}
u -> w: want
w -> a: 1
w -> b: 2
image

If we want all nodes to default to be gray, we can place

**: {style.fill: grey}

At the top of the file, and indeed all nodes are grey now, but this has now overridden our class settings, and flattened every node to the same style.fill value:

image

The other option is a glob class.

classes: {
  *: {
    style.fill: grey
  }
}
image

This almost works as expected (person is now grey, while decision is not), however a and b are still "unstyled". This is because the glob is applying to the entities in the classes block. This makes sense, however its use is a bit limited, since entities without a class don't get the attributes.

The way I see it, there are two options here:

  1. allow for special-pseudo classes such as default, which, when defined, will be applied at the top of the class stack for all entities within that block. I'd probably suggest adding three options: all, shape and edge.
  2. make the meaning of * special in class blocks

Both of these approaches have their benefits and drawbacks. Option 1 has the potential to conflict with any existing diagrams that use these names as normal classes, however it's probably easier to implement and it makes a lot of sense. Option 2 changes existing glob semantics, and probably requires changes in both the glob handling layer and the later stages of the parser; however, it will likely cause less issues with backwards compatibility, since I imagine a lot of people don't know you can use globs like that, or they just don't find it useful enough.

There is probably a third option, which would be to use a sigil such as ! to specify pesudo-classes. Ideally, this would be a currently-disallowed character; however the d2 syntax is very permissive since it doesn't have any expressions to account for, so pretty much anything other than # (comments), @ (imports) and * (globs) is already allowed and could conflict with prior work.

Personally, I'd vote for !, and then disallow its usage outside of class blocks and disallow any class name that begins with this character, except for !all !shape or !edge. If you like, you could also introduce a flag or pragma to turn this behavior off to allow for older diagrams to be compatible.

image

The nice thing about this is it can be made to work by dividing it into two stages:

  1. identifier exclusion in the parser, which would disallow other !-prefixed classes, while transparently removing the prefix (e.g changing !shape to shape)
  2. attaching the appropriate class to all items by default as whichever stage this needs to happen (i.e. everything gets class all while shapes additionally get shape and connection paths get edge)

This could also mean that items in a generated svg would have classes like

<g id="u" class="all shape actor"><path d="M 109 71 H 61..." />

as opposed to

<g id="u" class="actor"><g class="shape" ><path d="M 109 71 H 61..." />

which is how it's currently done.

This might actually end up being a good thing in terms of making svgs more selectable, though I don't know all of the implications. If !shape is a problem, you could always go with !node, which would make it

<g id="u" class="all node actor"><g class="shape" ><path d="M 109 71 H 61..." />

Which might also be more proper, depending on how hung up on graph terminology you are ;)

@cyborg-ts cyborg-ts added this to D2 Dec 2, 2023
@alixander
Copy link
Collaborator

Oh good point with the first example.

  1. Classes should take priority over globs.
  2. ** and *** should not apply to class definitions.

The below should give red.

**.style.fill: green
classes: {
  x: {
    style.fill: red
  }
}

a.class: x
Screen Shot 2023-12-06 at 6 16 57 PM

https://play.d2lang.com/?script=0tLSKy6pzEnVS8vMybFSSC9KTc3jSs5JLC5OLbZSqOZSUKiAUAoKyOqKUlO4FBRquWq5uBL1wMqtFCq4AAEAAP__&

@alixander
Copy link
Collaborator

As for pseudo-classes, I don't see the need. You can just define multiple classes and apply multiple as needed.

classes: {
  all: ...
  shapes: ...
  connections: ...
}

a.class: [all; shapes]
a -> b: {
  class: [all; connections]
}

@flowchartsman
Copy link
Author

flowchartsman commented Dec 7, 2023

** and *** should not apply to class definitions.
You can just define multiple classes and apply multiple as needed.

Except now there's still no default styling defined in one place, and you have to manually apply the all class to every otherwise un-classed entity and edge. That works, but it's a lot of boilerplate that's multiplied by every file you need to do it to, and that's easy to get wrong or omit by accident. It also makes changes to the default styling more difficult to apply, since they have to be applied to every single file.

At its core, this issue is about default styles that are easily applied to multiple graphs, so it could also be solved by allowing user-defined themes and extending theme support to include default style attributes on nodes, edges or both. Concrete use cases would be anyone who is working with a lot of graphs that they want have identical styling for. This might be someone who is writing a lengthy design proposal, an academic paper, or someone like me who is writing a book. So long as there is a convenient way to do default styles without a load of identical class definitions in every file, I think that's a win.

Personally, I think the theme idea is probably the best solution, followed by pseudoclasses, which could be included from another file, and finally glob improvements, which are the least intuitive of the three, since the syntax and scope are a bit abstruse , and the glob syntax for "all edges" is kind of awkward to use right now.

@alixander
Copy link
Collaborator

Except now there's still no default styling defined in one place, and you have to manually apply the all class to every otherwise un-classed entity and edge.

This should do it, right? Or am I missing a case you have in mind.

classes: {
  all: {
    style.font-size: 20
  }
}
**: {
  class: all
}
(** -> **)[*]: {
  class: all
}

someone like me who is writing a book

!!

@flowchartsman
Copy link
Author

This should do it, right? Or am I missing a case you have in mind.

As long as there is a fix that makes it so in this diagram a is red and b is green, that meets the basic use case, but I think user themes would still be the better way to do this, since I could just put the style def in a config file and be done with it instead of needing to copy that code to every file or otherwise include it. But, yes, if that works, that's definitely an improvement!

@alixander
Copy link
Collaborator

The plan to customize user themes is with vars.

E.g.

vars: {
  d2-config: {
    theme: {
      # This would overwrite the color code for neutral in the theme
      N1: "#FFF"
    }
  }
}

So you'd have to define this in a file and import it everywhere anyway, same as the glob method.

@flowchartsman
Copy link
Author

flowchartsman commented Dec 7, 2023

If it can be done in a single line, I think that's probably okay, though it would obviously be nicer if the theme flag could point to a file instead, or even if there were an include flag to allow processing and reprocessing many files at once using the same theme without needing to include it in the file. And, of course, the themes really should support base styling for all stylable entity classes. It's already kind of hacked in there for sketch, but it would be nice if it were more comprehensive. :)

@alixander
Copy link
Collaborator

separating this out into more dedicated issue: #1766

@alixander alixander closed this as not planned Won't fix, can't repro, duplicate, stale Dec 8, 2023
@github-project-automation github-project-automation bot moved this to Done in D2 Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

No branches or pull requests

2 participants