• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
1

How to create a Grep to apply style to variable number?

Contributor ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

I created a way to use data merge to populate multiple numbers to automate bar charts in InDesign based on the number that was pulled in. I feel like there must be a simpler code for it but i cant figure it out. (I put the visual in for reference and colored the dashes so you can see - final product has the dashes clear.Screenshot 2024-08-12 at 8.27.58 AM.png

The code basically says "Look behind for # tab and assign character style to # dashes" but I have to repeat the code 100 times and specify each number. is there a way to simplify where the code is more intuitive to say "read the number and assign the character style to that many dashes? 

 

 

(?<= 1\t)-{1}|(?<= 2\t)-{2}|(?<= 3\t)-{3}|(?<= 4\t)-{4}|(?<= 5\t)-{5}|(?<= 6\t)-{6}|(?<= 7\t)-{7}|(?<= 8\t)-{8}|(?<= 9\t)-{9}|(?<= 10\t)-{10}|(?<=11\t)-{11}|(?<=12\t)-{12}|(?<=13\t)-{13}|(?<=14\t)-{14}|(?<=15\t)-{15}|(?<=16\t)-{16}|(?<=17\t)-{17}|(?<=18\t)-{18}|(?<=19\t)-{19}|(?<=20\t)-{20}|(?<=21\t)-{21}|(?<=22\t)-{22}|(?<=23\t)-{23}|(?<=24\t)-{24}|(?<=25\t)-{25}|(?<=26\t)-{26}|(?<=27\t)-{27}|(?<=28\t)-{28}|(?<=29\t)-{29}|(?<=30\t)-{30}|(?<=31\t)-{31}|(?<=32\t)-{32}|(?<=33\t)-{33}|(?<=34\t)-{34}|(?<=35\t)-{35}|(?<=36\t)-{36}|(?<=37\t)-{37}|(?<=38\t)-{38}|(?<=39\t)-{39}|(?<=40\t)-{40}|(?<=41\t)-{41}|(?<=42\t)-{42}|(?<=43\t)-{43}|(?<=44\t)-{44}|(?<=45\t)-{45}|(?<=46\t)-{46}|(?<=47\t)-{47}|(?<=48\t)-{48}|(?<=49\t)-{49}|(?<=50\t)-{50}|(?<=51\t)-{51}|(?<=52\t)-{52}|(?<=53\t)-{53}|(?<=54\t)-{54}|(?<=55\t)-{55}|(?<=56\t)-{56}|(?<=57\t)-{57}|(?<=58\t)-{58}|(?<=59\t)-{59}|(?<=60\t)-{60}|(?<=61\t)-{61}|(?<=62\t)-{62}|(?<=3\t)-{63}|(?<=64\t)-{64}|(?<=65\t)-{65}|(?<=66\t)-{66}|(?<=67\t)-{67}|(?<=68\t)-{68}|(?<=69\t)-{69}|(?<=70\t)-{70}|(?<=71\t)-{71}|(?<=72\t)-{72}|(?<=73\t)-{73}|(?<=74\t)-{74}|(?<=75\t)-{75}|(?<=76\t)-{76}|(?<=77\t)-{77}|(?<=78\t)-{78}|(?<=79\t)-{79}|(?<=80\t)-{80}|(?<=81\t)-{81}|(?<=82\t)-{82}|(?<=83\t)-{83}|(?<=84\t)-{84}|(?<=85\t)-{85}|(?<=86\t)-{86}|(?<=87\t)-{87}|(?<=88\t)-{88}|(?<=89\t)-{89}|(?<=90\t)-{90}|(?<=91\t)-{91}|(?<=92\t)-{92}|(?<=93\t)-{93}|(?<=94\t)-{94}|(?<=95\t)-{95}|(?<=96\t)-{96}|(?<=97\t)-{97}|(?<=98\t)-{98}|(?<=99\t)-{99}|(?<=100\t)-{100}

 

<Title renamed by moderator>

 

TOPICS
How to , Scripting

Views

456

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 1 Correct answer

Community Expert , Aug 15, 2024 Aug 15, 2024

@LynxKx yes! Excellent news! You got me inspired to write a script along these lines, so here it is below. 

 

If anyone wants to give it a try, to please open the attached demo.indd and try that first. Here's a screen shot of before and after running script on demo document:

demo.gif

You can easily make your own graph bars or pies in your own document, but at first it might be easiest to look at mine or even copy/paste them. They are very simple: a bar element is a bunch of coloured rectangles pasted in

...

Votes

Translate

Translate
Community Expert ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

This is doable (reasonably) only with a script. Create a single character style for the bar (change 'Bar' in the code, below, to your name), the script then calculates where it should be applied.

 

 

cstyle = app.activeDocument.characterStyles.item('Bar');
app.findGrepPreferences = null;
app.findGrepPreferences.findWhat = '^\\d+';
nums = app.activeDocument.findGrep();
for (var i = 0; i < nums.length; i++) {
  nums[i].paragraphs[0].characters.itemByRange (
    nums[i].contents.length+1,
    Number(nums[i].contents) + nums[i].contents.length
  ).appliedCharacterStyle = cstyle;
}

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Hi Peter,

 

I think the op would like to keep his Grep code [that is correct and could be used inside a Grep style] But apparently he would like to be able to write such a code more "reasonably", I mean more fastly and more simply!  😉

 

… maybe with just 1 click!

 

/*
    _FRIdNGE-0756_Grep.jsx
    Script written by FRIdNGE, Michel Allio [12/08/2024]

    See:
    https://community.adobe.com/t5/indesign-discussions/grep-to-apply-style-to-variable-number/m-p/14794935#M584632
*/

// Place the cursor inside a text frame and run the Script.

var mySel = app.selection[0];
var P = 100,  p;
for ( p = 1; p <= P; p++ ) {
    if ( p == 1 ) mySel.contents = " " + p + "\\t\\K-{" + p + "}";
    else if ( p > 1 && p < 10 ) mySel.contents += "| " + p + "\\t\\K-{" + p + "}";
    else mySel.contents += "|" + p + "\\t\\K-{" + p + "}";
}
alert( "Just Copy/Paste the Text now! … ;-)\r\rby FRIdNGE, Michel Allio [12/08/2024]" )

 

(^/)  The Jedi

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Capture d’écran 2024-08-12 à 17.00.30.png

 

(^/)

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Well, LynxKx asked "is there a way to simplify where the code is more intuitive to say "read the number and assign the character style to that many dashes?"", to which the answer is still 'No.' Your script automates the writing of his code, but he already has it, so why bother? My script is the general case he asked about.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Really appreciate the help ... (I'm a She btw) ... I do know this can be done with a script, I woud like to keep it in grep so changes are instantaneos, Then it allows my editor to jump in InCopy and make realtime changes to the numbers and have the bar reflect the change. I was hoping my grep code in the character style could be simplified but stay in grep ... 

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Yes, ok, if you want it in a Grep style then you'll have to use your (clever) code. (Apologies for the pronoun!)

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

all good all good ... appreciate you both taking the time to help me learn!

its bittersweet ... I'm glad I coded the grep properly ... but sad there isnt a simpler grep. That would have opened up so many more posibilites! 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Hi @LynxKx, I think your idea is really clever!

 

(Side note: your grep is already about as simple as it can get—but I think you were hoping for something more concise, which is a different goal, and rarely worth pursuing in it's own right unless it is also more performant. Simple code such as yours is often quicker for the computer to process that concise, tricky code.)

 

I liked your idea so much I mocked it up myself and I hope you don't mind if I post it here for anyone who comes along to save them some time getting to where you got already.

 

Some notes on my implementation:

  • I used ^ in the grep rather than a space as delimiter, because all my numbers are at the start of the line.
  • I used strikethrough to perform the colouring so it could be somewhat uncoupled with the font used for the hyphens.
  • I set the paragraph style "Bar Graph" to be Forced Justified to the graph width will match the width of the column.
  • I tracked the hyphens very tightly so that the column measure can be quite small before the lines break up.
  • I set the "passive" grep specifically to 100 hyphens so that it will show up errors where there are too many hyphens accidentally present. (If you type an extra hyphen or more, they will appear at the end of the bar as normal hyphens.)
  • I didn't mock up the data merge, so it will need to have the data merge fields added in place of the numbers in mine.

 

See my demo.indd attached. This was a quick attempt and I'm sure there are further improvements possible, so please let us know what else you end up doing to make this technique work. Thanks again for sharing your brilliant idea!

- Mark

 

These bars are all the exact same paragraph style.These bars are all the exact same paragraph style.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Absolutely! I love figuring out tricky solutions in InDesign. I used a rule for the background coloring and also used strikethrough for the bar coloring. In my original project I used a space before the number because I automated the coloring as well so I only needed one paragraph style, if it was B 16 then the coloring would be blue, G 16 then the coloring would be green and so on. I was only able to fit 4 different colors before InDesign started getting mad at me for having so many options. 

 

You can do the same thing to create a donut graph, just put a basline shift on the strikethrough until you dont have a gap in the solid chunks.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

Wow! A donut graph? Amazing. Do you have a screen shot? I can't quite imagine it.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 12, 2024 Aug 12, 2024

Copy link to clipboard

Copied

I think I've got it! I put the number in its own frame, linked to type-on-a-path, separated by a line feed. And tabs so that the number is centred and not force-justified.

 

Screenshot 2024-08-13 at 12.16.04.pngScreenshot 2024-08-13 at 12.17.03.png

Is that something like what you mean? Very impressive @LynxKx! A fun exercise for me to puzzle out. Updated demo.indd attached for anyone else interested.

- Mark

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 13, 2024 Aug 13, 2024

Copy link to clipboard

Copied

Thank you! 🙂 I love that you're working through this! I could spend my whole day doing stuff like this!

I keep the number i'm pulling from really tiny and still in the same format as the bar ... you can do it the way you have it and works great when typing in - but using data merge you have to go in and trigger the grep. I've attached a demo file. Top donut has the number and dashes in black, but final product I dont have any color assigned to those numbers. For the data number I adjust the tab so the dash will align at 1 position, and have the end stop at 99, since I would just do a full fill if it were 100. For the larger number I just have a circle text frame placed into the first text frame so I can use a fill for the background, then just type the number I'm representing ... sometimes it will a stat callout. The large number in mine isnt connected to the donut but if you use data merge its not extra work - you could also make it variable based on the donut number.

Screenshot 2024-08-13 at 8.37.16 AM.pngScreenshot 2024-08-13 at 8.38.45 AM.png

Another way to do the donut graphs... that doesnt use the number to autfill, this helps when you have more than 2 data points. Use the nested styles to color the bars, it requires an override on the style but I don't mind it for this stuff.

Screenshot 2024-08-13 at 9.07.24 AM.pngScreenshot 2024-08-13 at 9.05.25 AM.pngScreenshot 2024-08-13 at 9.14.32 AM.png

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 14, 2024 Aug 14, 2024

Copy link to clipboard

Copied

Really great stuff! Nice illustration of how you can persuade InDesign to do things it didn't know it was capable of.

Nevertheless, and I don't want to detract from your heroic efforts, but do you know the Chartwell fonts?

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 14, 2024 Aug 14, 2024

Copy link to clipboard

Copied

@Peter Kahrel yeah! Chartwell fonts are amazing!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 14, 2024 Aug 14, 2024

Copy link to clipboard

Copied

I am familiar with chartwell fonts but my company didnt want to spend the money and if i can figure out an alternative then why not 🙂

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 14, 2024 Aug 14, 2024

Copy link to clipboard

Copied

That's awesome @LynxKx! Using nested styles like that is a nice simple approach. This whole graph journey you started has been quite fun for me. Thanks. 🙂

 

P.S. I'm kind of mental about scripting, I love to write a script most days if I get any downtime. Well, today you inspired me to write a graphing script, just to see how I would solve this in a script way. It is quite basic—doesn't do much more than my simple demo document above—but it turned out really well I think, and is surprisingly easy to set up and style. Do you do any scripting, or use scripts? If you're interested in seeing yet another(!) approach I will post it on this thread with a demo document.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 14, 2024 Aug 14, 2024

Copy link to clipboard

Copied

I love scripting! Been learning as I go though over the years, I figure out what I want to do and study scripts to figure it out to make new ones, and when I get stuck, adobe community is a huge life line! greatful when I get to contribute to it!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 15, 2024 Aug 15, 2024

Copy link to clipboard

Copied

@LynxKx yes! Excellent news! You got me inspired to write a script along these lines, so here it is below. 

 

If anyone wants to give it a try, to please open the attached demo.indd and try that first. Here's a screen shot of before and after running script on demo document:

demo.gif

You can easily make your own graph bars or pies in your own document, but at first it might be easiest to look at mine or even copy/paste them. They are very simple: a bar element is a bunch of coloured rectangles pasted into another rectangle, and a pie element is a bunch of coloured polygons pasted into a frame, usually a circle.

 

The elements are just a page item anchored nearby to the graph value text,usually within a few characters (the script will connect the closest one found).

 

Let me know what you think.

- Mark

 

 

/**
 * @file Update Graph Elements.js
 *
 * Update graph elements (bars, lines or pie slices) to reflect
 * the associated text value(s), either manually entered, or populated
 * with data merge.
 *
 * IMPORTANT: very little testing done on this script!
 * Please let me know when you find bugs.
 * 
 * Disclaimer: this is NOT a comprehensive graphing solution.
 *
 * Usage:
 * 
 *  - Run script in a document containing graph elements (see demo.indd).
 *    Script will update graphs in the selection or, if no selection,
 *    will update ALL graphs in document.
 *
 * The script works this way:
 *
 *  1. Finds all instances of graph values, which are numbers, or set of
 *     numbers, with the "Graph Value" Character Style applied.
 *
 *  2. Finds any anchored graph elements (pie or bar) near
 *     the character (by default it looks two characters
 *     either side).
 *
 *  3. Adjusts each graph element to represent the value.
 *
 * Configure your `settings` object:
 *  • findWhat: a findGrep compatible string goes here.
 *  • searchProperties: an object containing any findGrepPreferences properties.
 *  • showResults: whether to show an alert at the end.
 *  • reset: sets all the graphs to zero, no matter what their value.
 *
 * Notes:
 *
 *  - A graph "bar" is just a rectangle pasted inside another
 *    rectangle. Make them different colours!
 *
 *  - A graph pie "slice" is a closed polygon pasted inside any kind of
 *    frame, usually a circle or a donut. Add as many slices as you need
 *    for your graph and style each slice as you like.
 *
 *  - A graph "line" is an open polygon pasted inside any kind of frame,
 *    usually a rectangle. Add as many lines as you need for your graph
 *    and style each slice as you like.
 *
 *  - To embed multiple polygons (or whatever page item you want) into the
 *    graph frame, it is easiest to group it and then "Edit > Paste Into".
 *    This script will try to find the graph elements inside that group.
 *
 *  - A graph bar, pie, or line graph can have multiple values: to do
 *    multi-value charts, separate them by a single, non-digit character,
 *    eg. "10 20 50", but you must set the delimiter in "Graph Value"
 *    Character Style also. The delimiter could be a space, a line feed,
 *    or column break character, for example.
 *  
 *    Caution: If you are making a bar graph with multiple bars, each with
 *    a single-value for each bar, you need either (a) two or more characters
 *    between each value, or (b) one or more characters WITHOUT the "Graph Value"
 *    Character Style applied. Failure to do this may cause the script to
 *    populate the bar(s) as multi-value bars.
 *
 *  – The anchored graph element can be pasted either before or after
 *    the value(s) text.
 *
 *  - The script leaves styling of elements to the user.
 * 
 *  - Create enough graph elements (eg. polygons pasted inside a circle frame
 *    for a Pie) for the largest number of multi-value graphs. The script will
 *    create extra bars, lines or slices as needed, but it won't style them,
 *    so it will save you time to set up a "master" graph element complete with
 *    styled (using Object Styles is a good idea) bars, lines or slices.
 * 
 *    Example creating a pie graph element suitable for showing six slices:
 *      1. Make a circle frame.
 *      2. Draw a closed triangle with pen tool (if it is open, then script will
 *         populate as a line graph).
 *      3. Draw and style six triangles (this is when you would apply Object Styles).
 *         Note: we don't care about the size or position of the triangles at all!
 *         Also if you make too many triangles, that's fine - the script will hide
 *         any elements that don't have values assigned.
 *      4. Group the six triangles.
 *      5. Cut and "Edit > Paste Into" the selected circle frame from step 1.
 *      6. Cut the circle from step 1, and with Type tool, place an insertion point
 *         within 2 characters of your graph values text, eg. on either x is good:
 *         "x20 30 40x", and paste to anchor the graph element.
 *      7. Configure the Anchored Object Options to suit (again, I recommend using
 *         an Object Style).
 * 
 * -  Tip: in a datamerge
 *    template document start with graph elements that already have the
 *    internal polygons/rectangles colored as desired or, even better, have
 *    object styles applied. It is fine to have more bars or slices or lines
 *    than you need - they will be hidden by the script.
 *
 *  - One good use case for this script is to use in conjunction with datamerge:
 *    set the graph data merge placeholders in "Graph Value" Character Style
 *    and, after merging, run this script to update the graphs.
 *
 * Configuring via Script Labels:
 *
 * To access some simple configuration options, add key/value pairs
 * to the script label of either the anchored graph element or to the
 * parent text frame. See Window > Utilities > Script Label menu.
 *
 * List of keys:
 *
 * max - sets a upper range for graph values. eg. "max:250;"
 *       Default is 100 for single-value graphs, or the sum of all
 *       values when there are multi-values. Values above this
 *       range will be cropped.
 *
 * min - sets a lower range for graph values. eg. "min:-50;"
 *       Default is 0 for single-value graphs. When multi-values
 *       are present "min" is not used. Values below this range
 *       will be cropped
 *
 * lineCount - in a line graph, the number of lines in the graph, if more than one.
 *
 * vertical - in a line graph, to force the graph into vertical orientation.
 *
 * horizontal - in a line graph, to force the graph into horizontal orientation.
 *
 * filled - a line graph, sets whether a line's path will be drawn so that
 *          it can be filled. You must style the line polygon yourself.
 *
 * flip - in a line graph, whether to flip the axis of the data values.
 *
 * nubSize - in a line graph, the length of the little extension on either edge.
 *
 * overshoot - in a pie graph, how far outside the normal radius the slices extend.
 *
 * startAngle - in a pie graph sets the angle for the first slice.
 *              Default is 0 (pointing right). For example, to set the
 *              start angle pointing up, add label "startAngle:90;"
 *
 * @author m1b
 * @version 2024-08-14
 * @inspiration the cool graphs made by @LynxKx at URL below
 * @discussion https://community.adobe.com/t5/indesign-discussions/grep-to-apply-style-to-variable-number/m-p/14794935
 */

/**
 * Script settings, currently configured
 * to search for positive numbers set in
 * the 'Graph Value' Character Style.
 */
var settings = {
    findWhat: '([\\d,\\.-]\\D?)+',
    searchProperties: { appliedCharacterStyle: 'Graph Value' },
    showResults: true,
    // reset: true,
};

/**
 * A graph bar element.
 * @author m1b
 * @version 2024-08-14
 * @constructor
 * @param {PageItem} item - an item containing a rectangle.
 * @returns {GraphBar}
 */
function GraphBar(item) {

    var self = this;
    self.item = item;
    self.container = GraphBar.getContainer(item);
    self.isValid = undefined != self.container;

    if (!self.isValid)
        return;

    self.bounds = item.geometricBounds;

    // check labels for key/value pairs
    var data = parseKeyValues(
        item.label + ';'
        + item.parent.parentTextFrames[0].label
    );

    // value range
    self.max = getAsNumber(data.max, 100);
    self.min = getAsNumber(data.min, 0);

};

/**
 * Returns true if the item meets
 * the criteria to be a GraphBar.
 * @param {PageItem} item - the item to check.
 * @returns {Boolean}
 */
GraphBar.is = function is(item) {
    return undefined != GraphBar.getContainer(item);
};

/**
 * Returns a container of rectangles.
 * If no bars are found, returns undefined.
 * @param {PageItem} item - the item to search.
 * @returns {PageItem?} - the container.
 */
GraphBar.getContainer = function getContainer(item) {

    var container;

    if (item.rectangles[0].isValid)
        container = item;

    else if (
        item.groups[0].isValid
        && item.groups[0].rectangles[0].isValid
    )
        container = item.groups[0];

    return container;

};

/**
 * Returns array of `count` bar elements.
 * Adds or removes elements as necessary.
 * @author m1b
 * @version 2024-08-15
 * @param {Number} count - the number of elements desired.
 * @returns {Array<Polygon>}
 */
GraphBar.prototype.getElements = function getElements(count) {
    var self = this;
    return getElementsOfContainer(self.container, 'rectangles', count);
};

/**
 * Adjusts the bar according to `value`;
 * @author m1b
 * @version 2024-08-14
 * @param {Number|Array<Number>} values - the graph value(s).
 */
GraphBar.prototype.setValue = function setValue(values) {

    var self = this;

    if ('Number' === values.constructor.name)
        values = [values];

    var max = 1 === values.length ? self.max || 100 : sum(values),
        min = 1 === values.length ? self.min || 0 : 0;

    // adjust the bars to match the values
    var b = self.bounds,
        width = b[3] - b[1],
        height = b[2] - b[0],
        vertical = width < height;

    var elements = self.getElements(values.length);

    // adjust the bar sizes
    for (var i = 0, adv = 0, value, bb, start = 0, end; i < elements.length; i++) {

        value = 1 === values.length ? values[i] : Math.abs(values[i]);

        if (
            i >= values.length
            || 0 === values[i]
        ) {
            // unused element, move it out of the way
            elements[i].geometricBounds = [0, 0, 1, 1];
            continue;
        }

        start = getScaleFactor(max, min, adv) * (vertical ? height : width);
        end = getScaleFactor(max, min, value + adv) * (vertical ? height : width);

        if (vertical) {
            bb = [
                b[2] - Math.max(start, end),
                b[1],
                b[2] - Math.min(start, end),
                b[3],
            ];

            adv += value;
        }

        else {
            bb = [
                b[0],
                b[1] + Math.max(start, end),
                b[2],
                b[1] + Math.min(start, end),
            ];

            adv += value;
        }

        elements[i].geometricBounds = bb;

    }

};

/**
 * A graph line element.
 * @author m1b
 * @version 2024-08-16
 * @constructor
 * @param {PageItem} item - an item containing an open path.
 * @returns {GraphLine}
 */
function GraphLine(item) {

    var self = this;
    self.item = item;
    self.container = GraphLine.getContainer(item);
    self.isValid = undefined != self.container;

    if (!self.isValid)
        return;

    self.bounds = self.item.geometricBounds;
    self.line = self.container.polygons[0];

    // check labels for key/value pairs
    var data = parseKeyValues(
        item.label + ';'
        + item.parent.parentTextFrames[0].label
    );

    // whether to flip the data axis
    self.flip = getAsNumber(data.flip);

    // value range
    self.max = getAsNumber(data.max, 100);
    self.min = getAsNumber(data.min, 0);

    // force vertical
    self.lineCount = getAsNumber(data.lineCount, 1);

    // size of the leading and trailing sections of the line
    self.nubSize = getAsNumber(data.nubSize, 5);

    // orientation of line
    self.vertical = getAsBoolean(data.vertical, undefined);

    if (data.horizontal)
        self.vertical = false;

    // whether to flip the data axis
    self.flip = getAsBoolean(data.flip);

    // whether the line is to be filled
    self.filled = getAsBoolean(data.filled);

};

/**
 * Returns true if the item meets
 * the criteria to be a GraphLine.
 * @param {PageItem} item - the item to check.
 * @returns {Boolean}
 */
GraphLine.is = function is(item) {
    return undefined != GraphLine.getContainer(item);
};

/**
 * Returns a container of polygons.
 * If no lines are found, returns undefined.
 * @param {PageItem} item - the item to search.
 * @returns {PageItem?} - the container.
 */
GraphLine.getContainer = function getContainer(item) {

    var container;

    if (item.polygons[0].isValid)
        container = item;

    else if (
        item.groups[0].isValid
        && item.groups[0].polygons[0].isValid
    )
        container = item.groups[0];

    if (
        container
        && container.polygons[0].paths.length > 0
        && PathType.OPEN_PATH === container.polygons[0].paths[0].pathType
    )
        return container;

};

/**
 * Returns array of `count` line elements.
 * Adds elements to make up `count` if necessary.
 * @author m1b
 * @version 2024-08-15
 * @param {Number} count - the number of elements desired.
 * @returns {Array<Polygon>}
 */
GraphLine.prototype.getElements = function getElements(count) {
    var self = this;
    return getElementsOfContainer(self.container, 'polygons', count);
};

/**
 * Adjusts the line according to `values`;
 * @author m1b
 * @version 2024-08-16
 * @param {Number|Array<Number>} values - the graph value(s).
 */
GraphLine.prototype.setValue = function setValue(values) {

    var self = this;

    if ('Number' === values.constructor.name)
        values = [values];

    if (values.length < 2)
        // not enough values to draw line
        return;

    var max = self.max || Math.max.apply(null, values),
        min = self.min || Math.min.apply(null, values),
        flip = self.flip === true;

    var b = self.bounds.slice(),
        width = b[3] - b[1],
        height = b[2] - b[0],
        vertical = undefined != self.vertical
            ? self.vertical
            : width < height;

    var lines = self.getElements(self.lineCount),
        valuesPerLine = Math.ceil(values.length / self.lineCount),
        step = (vertical ? height : width) / (valuesPerLine - 1);

    // adjust the lines
    for (var i = 0, adv, points; i < lines.length; i++) {

        adv = 0;
        points = [];

        for (var j = 0, p, value; j < valuesPerLine; j++) {

            value = values[i * valuesPerLine + j];

            // calculate the point on the data axis
            p = getScaleFactor(max, min, value) * (vertical ? width : height);

            if (vertical) {
                points.push(flip
                    ? [b[3] - p, b[0] + adv]
                    : [b[1] + p, b[0] + adv]
                );
            }
            else {
                points.push(flip
                    ? [b[1] + adv, b[0] + p]
                    : [b[1] + adv, b[2] - p]
                );
            }

            adv += step;

        }

        // add a little nub at both ends
        if (vertical) {
            points.unshift([points[0][0], points[0][1] - self.nubSize]);
            points.push([points[points.length - 1][0], points[points.length - 1][1] + self.nubSize]);
        }

        else {
            points.unshift([points[0][0] - self.nubSize, points[0][1]]);
            points.push([points[points.length - 1][0] + self.nubSize, points[points.length - 1][1]]);
        }

        if (self.filled) {

            // add points at both ends to "fill" the line
            var overshoot = 5,
                h = flip ? b[0] - overshoot : b[2] + overshoot,
                v = flip ? b[3] + overshoot : b[1] - overshoot;

            if (vertical) {
                points.unshift([v, points[0][1]]);
                points.push([v, points[points.length - 1][1]]);
            }

            else {
                points.unshift([points[0][0], h]);
                points.push([points[points.length - 1][0], h]);
            }

        }

        // adjust the line to match the values
        lines[i].paths[0].entirePath = points;

    }

};

/**
 * A graph pie element.
 * @author m1b
 * @version 2024-08-14
 * @constructor
 * @param {PageItem} item - an item containing a polygon.
 * @returns {GraphPie}
 */
function GraphPie(item) {

    var self = this;
    self.item = item;
    self.container = GraphPie.getContainer(item);
    self.isValid = undefined != self.container;

    if (!self.isValid)
        return;

    self.bounds = item.geometricBounds;

    // check labels for key/value pairs
    var data = parseKeyValues(
        item.label + ';'
        + item.parent.parentTextFrames[0].label
    );

    // value range
    self.max = getAsNumber(data.max, 100);
    self.min = 0;

    // the angle of the first slice
    self.startAngle = getAsNumber(data.startAngle, 0);

    // how far the radius overshoots the container bounds
    self.overshoot = getAsNumber(data.overshoot, undefined);

};

/**
 * Returns true if the item meets
 * the criteria to be a GraphPie.
 * @param {PageItem} item - the item to check.
 * @returns {Boolean}
 */
GraphPie.is = function is(item) {
    return undefined != GraphPie.getContainer(item);
};

/**
 * Returns a container of slice polygon(s).
 * If no slices are found, returns undefined.
 * @param {PageItem} item - the item to search.
 * @returns {PageItem?} - the container.
 */
GraphPie.getContainer = function getContainer(item) {

    var container;

    if (item.polygons[0].isValid)
        container = item;

    else if (
        item.groups[0].isValid
        && item.groups[0].polygons[0].isValid
    )
        container = item.groups[0];

    if (
        container
        && container.paths.length > 0
        && PathType.CLOSED_PATH === container.polygons[0].paths[0].pathType
    )
        return container;

};

/**
 * Returns array of `count` pie slice elements.
 * Adds elements to make up `count` if necessary.
 * @author m1b
 * @version 2024-08-15
 * @param {Number} count - the number of elements desired.
 * @returns {Array<Polygon>}
 */
GraphPie.prototype.getElements = function getElements(count) {
    var self = this;
    return getElementsOfContainer(self.container, 'polygons', count);
};

/**
 * Adjusts the inner polygon(s) of
 * the GraphPie to show pie slices.
 * @author m1b
 * @version 2024-08-15
 * @param {Number|Array<Number>} values - the graph value(s).
 */
GraphPie.prototype.setValue = function setValue(values) {

    var self = this;

    if ('Number' === values.constructor.name)
        values = [values];

    var max = 1 === values.length ? 100 : sum(values),
        angles = [];

    // calculate angle for each value
    for (var i = 0, value; i < values.length; i++) {
        value = Math.max(0, values[i]);
        angles.push(value / max * 360);
    }

    var b = self.bounds,
        centerX = (b[1] + b[3]) / 2,
        centerY = (b[0] + b[2]) / 2,
        radius = (b[3] - b[1]) / 2;

    // add overshoot, default 10%
    radius += (self.overshoot ? self.overshoot : radius * 0.1);

    var slices = self.getElements(angles.length);

    // adjust the pie slices
    for (var i = 0, start = self.startAngle; i < slices.length; i++) {

        if (
            i >= values.length
            || 0 === values[0]
        ) {
            // unused element, move it out of the way
            slices[i].paths[0].entirePath = [[b[0], b[1]], [b[0], b[1]]];
            continue;
        }

        var end = (start + angles[i]),
            startRad = start * Math.PI / 180,
            endRad = end * Math.PI / 180;

        var allAnglesRad = interpolateRange(startRad, endRad, Math.floor((end - start) / 45));

        var points = [[centerX, centerY]];

        for (var j = 0; j < allAnglesRad.length; j++)
            points.push([
                centerX + radius * Math.cos(allAnglesRad[j]),
                centerY + radius * Math.sin(allAnglesRad[j]),
            ]);

        // set the polygon's path
        slices[i].paths[0].entirePath = points;

        // ready for next slice
        start = end;

    }


};

/**
 * Returns an array including `count` interpolated
 * values between `start` and `end`, inclusive.
 * @param {Number} start - the first value.
 * @param {Number} end - the last value.
 * @param {Number} count - the number of interpolations.
 * @returns {Array<Number>}
 */
function interpolateRange(start, end, count) {

    var range = [start],
        step = (end - start) / (count + 1);

    for (var i = 1; i <= count; i++)
        range.push(start + i * step);

    range.push(end);

    return range;

};

/**
 * Returns the closest anchored object found near the given text.
 * @param {Text} text - the text associated with the graph element.
 * @param {Number} [buffer] - the search distance, in characters, around the text (default: 5).
 * @returns {GraphBar|GraphPie?}
 */
function getNearestGraphElement(text, buffer) {

    // look at chars on either side of value
    buffer = buffer || 5;

    // search around the text for graph elements
    for (var i = 1; i <= buffer; i++) {

        var start = Math.max(text.characters[0].index - i, 0),
            end = Math.min(text.characters[-1].index + i, text.paragraphs[0].characters[-1].index),
            textRange = text.parentStory.characters.itemByRange(start, end).getElements()[0],
            items = textRange.pageItems.everyItem().getElements(),
            item = undefined;

        while (items.length) {
            if (items[0].isValid) {
                item = items[0];
                break;
            }
            items.shift();
        }

        if (!item)
            continue;

        if (GraphBar.is(item))
            return new GraphBar(item);

        else if (GraphLine.is(item))
            return new GraphLine(item);

        else if (GraphPie.is(item))
            return new GraphPie(item);

    }

};

/**
 * Main script:
 *   1. Find graph values using `settings`.
 *   2. Find associated graph element (pie and bar)
 *      for each number (or number group)
 *   3. Adjust the graph elements to match the values.
 */
function main() {

    // expand search by number of characters on either size of graph values text
    const SEARCH_DISTANCE = 2;

    // any non-digit character
    const VALUE_DELIMITER = /[^\d,\.\|-]/g;

    // reset all graph elements to this value
    const RESET_VALUE = 0;

    if (0 === app.documents.length)
        return alert('Please open a document and try again.');

    var doc = app.activeDocument,
        counter = 0;

    app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS;
    app.findGrepPreferences = NothingEnum.NOTHING;
    app.findGrepPreferences.findWhat = settings.findWhat;

    if (settings.searchProperties)
        app.findGrepPreferences.properties = settings.searchProperties;

    var targets = doc.selection.length > 0 ? doc.selection : [doc],
        target
    done = {};

    while (target = targets.pop()) {

        if ('InsertionPoint' === target.constructor.name)
            // we want the story
            target = target.parent;

        if (
            target.hasOwnProperty('id')
            && done[target.id]
        )
            // already done
            continue;

        if (
            'Character' === target.parent.constructor.name
            && 'TextFrame' !== target.constructor.name
        )
            // might be an anchored graph element
            target = target.parent.parent;

        else if ('InsertionPoint' === target.constructor.name)
            // we want the story
            target = target.parent;

        else if ('Text' === target.constructor.name)
            // add any anchored text frames
            targets = targets.concat(target.textFrames.everyItem().getElements());

        if ('TextFrame' === target.constructor.name)
            // add any anchored text frames
            targets = targets.concat(target.textFrames.everyItem().getElements());

        if (!target.hasOwnProperty('findGrep'))
            // can't use this
            continue;

        done[target.id] = true;

        // find the graph values
        found = target.findGrep();

        for (var i = 0, text, nums, values, element; i < found.length; i++) {

            text = found[i];
            element = getNearestGraphElement(text,SEARCH_DISTANCE);

            if (!element)
                continue;

            // parse the number(s)
            values = [];
            nums = text.contents.split(VALUE_DELIMITER);

            for (var j = 0, num; j < nums.length; j++) {

                num = settings.reset ? RESET_VALUE : parseFloat(nums[j]);

                if (!isNaN(num))
                    values.push(num);

            }

            // adjust the graph element
            element.setValue(values);
            counter++;

        }

    }

    if (settings.showResults)
        alert('Updated ' + counter + ' graph elements.');

};
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, 'Update Graph Elements');


/**
 * Returns an array of `count` page items
 * of `typePlural` found in `container`.
 * Will add elements if necessary.
 * @author m1b
 * @vesion 2024-08-15
 * @param {Page Item} container - the container of the elements, eg. Rectangle.
 * @param {String} typePlural - the key to the page items, eg. "rectangles".
 * @param {Number} count - the number of elements desired.
 * @returns {Array<*>}
 */
function getElementsOfContainer(container, typePlural, count) {

    count = count || 1;

    if (!container.hasOwnProperty(typePlural))
        throw Error('getElementsOfContainer: bad `typePlural` supplied.');

    // create elements if necessary
    for (var i = 0; i < count; i++)
        if (!container[typePlural][i].isValid)
            container[typePlural].add();

    return container[typePlural].everyItem().getElements();

};


/**
 * Returns sum of numbers.
 * @param {Array<Number>} arr - the numbers to sum.
 * @returns {Number}
 */
function sum(arr) {

    var total = 0;

    for (var i = 0; i < arr.length; i++)
        total += arr[i];

    return total;

};

/**
 * Returns `str` as a Number, or `defaultNum` if fails.
 * @param {String} str - the number to coerce from string.
 * @param {Number} [defaultNum] - if coercing fails, return this (default: undefined).
 * @returns {Number?}
 */
function getAsNumber(str, defaultNum) {

    var num = Number(str);
    return isNaN(num) ? defaultNum : num;

};

/**
 * Returns `str` as a Boolean;
 * @param {String} str - the bolean to coerce from string.
 * @param {*} [defaultValue] - if coercing fails, return this (default: undefined).
 * @returns {Boolean}
 */
function getAsBoolean(str, defaultValue) {

    if (
        undefined == str
        || '' == str
    )
        return defaultValue;

    var num = Number(str);

    if ('true' === str.toLowerCase())
        return true;
    else
        return !isNaN(num) && num > 0;

};

/**
 * Returns the scale factor of `n` between `min` and `max`.
 * @param {Number} max - the maximum value.
 * @param {Number} min - the minimum value.
 * @param {Number} n - the value to be scaled.
 * @returns {Number}
 */
function getScaleFactor(max, min, n) {

    return (n - min) / (max - min);

};

/**
 * Returns an object populated by
 * key/value pairs parsed from `str`.
 *
 * Example:
 *   parseSimpleData('max:250;min:-25');
 *
 *   returns this object:
 *     {
 *        'max':'250',
 *        'min':'-25',
 *     }
 *
 * @author m1b
 * @version 2024-08-17
 * @param {String} str - the string to parse.
 * @returns {Object}
 */
function parseKeyValues(str) {

    const END_OF_KEY_CHAR = ':',
        END_OF_VALUE_CHAR = ';',
        MATCH_KEY_VALUE = new RegExp('(?^|\\n|' + END_OF_VALUE_CHAR + '\\s*)([a-z]+)\\s*' + END_OF_KEY_CHAR + '\\s*([^' + END_OF_VALUE_CHAR + ']+)\\s*', 'ig');

    var results = {},
        match;

    while (match = MATCH_KEY_VALUE.exec(str))
        if (3 === match.length)
            results[match[1]] = match[2];

    return results;

};

 

 

Edit 2024-08-16: fixed bug where GraphPie didn't check its parent text frame's label.

Edit 2024-08-18: improved code a bit here and there, added support for line graphs, and updated documentation and attached demo.indd.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 15, 2024 Aug 15, 2024

Copy link to clipboard

Copied

Cant wait to study your script!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Contributor ,
Aug 15, 2024 Aug 15, 2024

Copy link to clipboard

Copied

This looks AWESOME!!! Absolutely going to start working on this! Thank you! You Rock!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 15, 2024 Aug 15, 2024

Copy link to clipboard

Copied

LATEST

Nice! Let me know if you find bugs or make improvements! Have fun!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines