Coder Social home page Coder Social logo

Comments (35)

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Hello 😁 I understand your requirement pretty well. However, I am not sure if this is a good idea. It introduces a lot of complexity. Especially taking into account that PDF documents are static in their nature and don't need to be very responsive (in contrast with web design).

In your case, I would use the Grid component. You can specify your own number of columns (12 by default) to achieve better control over the layout with higher granularity. In this article, you have a great example of how to use the Grid component to create a complex layout.

from questpdf.

qcz avatar qcz commented on May 21, 2024

Thanks for the quick answer.

Hello 😁 I understand your requirement pretty well. However, I am not sure if this is a good idea. It introduces a lot of complexity. Especially taking into account that PDF documents are static in their nature and don't need to be very responsive (in contrast with web design).

These columns should always have fixed size in the end result. I don't think this is responsivity in the web sense, I do not want to size the table based on content. I just want to be able to:

create the following in one row:

row.ConstantColumn(100)
row.RelativeColumn(2)
row.ConstantColumn(24)

and create the next row with a merged column as big as the first two columns:

// write something here so the first column has s constant 100 + relative 2 width, so it matches the other rows
row.ConstantColumn(24)

like this:
EXCEL_2021-12-17_12-40-07

You already support relative columns as a "responsivity" feature, this is just the extension of that. Of course I did not yet read through the source code of QuestPDF, so I cannot determine the complexity of supporting something like this.

In your case, I would use the Grid component. You can specify your own number of columns (12 by default) to achieve better control over the layout with higher granularity. In this article, you have a great example of how to use the Grid component to create a complex layout.

Unfortunately I'm not seeing how that helps. I cannot define the size of the columns, they are all the same. If I could, it really would be okay, if grids supports paging & headers, but I am not sure it does any of that based on the documentation. Or I should declare 1000 columns and do the calculation myself? That seems very counter-intuitive.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Every element in QuestPDF supports paging by default if only it is possible, including the Grid element 😁 Assumig your example, you can do something like this:

.Grid(grid => 
{
    grid.Columns(16);

    // first row
    grid.Item(4).Text("Cell 1");
    grid.Item(9).Text("Cell 2");
    grid.Item(3).Text("Cell 3");

    // second row
    grid.Item(13).Text("Merged 1 + 2");
    grid.Item(3).Text("Cell 4");
})

I understand that cells 1 and 2 will not have precise size as in your mockup. Is it very important? Or can they just be close enough?

from questpdf.

qcz avatar qcz commented on May 21, 2024

Precision is very important for reports with many columns.

Anyway, I've implemented it for myself in qcz@ad58290 , and it is working just fine:

This:

decoration.Content().Stack((stack) =>
{
    stack.Item().Row(row =>
    {
        row.ConstantColumn(50).Text("Item 2");
        row.RelativeColumn(2).Text("SubType 1");
        row.ConstantColumn(50).Text("23.32");
        row.RelativeColumn(4).Text("Lorem ipsum dolor sit amet");
    });
    stack.Item().Row(row =>
    {
        row.ConstantColumn(50).Text("Item 2");
        row.RelativeColumn(2).Text("SubType 1");
        row.ConstantColumn(50).Text("44.44");
        row.RelativeColumn(4).Text("Testing text 1234");
    });
    stack.Item().Row(row =>
    {
        row.MixedColumn(constantWidth: 50, relativeWidth: 2).Text("Total");
        row.ConstantColumn(50).Text("67.76");
        row.RelativeColumn(4).Text("Testing text 2342");
    });
});

results in:
mstsc_2021-12-17_14-45-40

(Not 100% satisfied with the "mixed" naming though 😄)

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

(Not 100% satisfied with the "mixed" naming though 😄)

This is exactly my problem. It may be challenging to describe it in the documentation.

I just now realized that this functionality may be useful at times. And the solution you made is close to my idea of implementation, though it could be greatly simplified.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Plus, you can always do something like this with the existing API:

.Stack(x => {
    x.Item().Row(x => {
        x.RelativeColumn().Row(x => {
            x.ConstantColumn(200).Text("Cell 1");
            x.RelativeColumn().Text("Cell 2");
        });

        x.ConstantColumn(200).Text("Cell 3");
    });

    x.Item().Row(x => {
        x.RelativeColumn().Text("Merged 1 + 2");
        x.ConstantColumn(200).Text("Cell 4");
    });
});

This keeps the feature set small and easy to understand with a cost of slightly more code on the implementation side. On the other hand how the Mixed approach is not obvious too when you try to quickly visualize the output.

from questpdf.

qcz avatar qcz commented on May 21, 2024

Plus, you can always do something like this with the existing API:

Yeah, but I don't think you can do the one which was with the sample code I provided.

This is exactly my problem. It may be challenging to describe it in the documentation.

The merged cell in a table is good for explanation. I agree that this is an advanced use case. Maybe put it in an Advanced or LowLevel fluent namespace, so it won't scare away beginner users?

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

If I am not mistaken, the code in my last comment does exactly what you want :) But instead of merging, it performs a split. The result is the same.

from questpdf.

qcz avatar qcz commented on May 21, 2024

You are right. But as soon as there is an overlap between any merged cells, this won't work.
EXCEL_2021-12-18_12-34-22

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Thank you for finding this use case, it is a fair observation. I will investigate how to best implement this functionality. If everything goes right, it is going to be available in the next release :)

I still would prefer to create a real Table component with a column/row spanning - there are just many corner cases, especially with paging support in mind...

from questpdf.

qcz avatar qcz commented on May 21, 2024

I will investigate how to best implement this functionality. If everything goes right, it is going to be available in the next release :)

Thank you!

I still would prefer to create a real Table component with a column/row spanning - there are just many corner cases, especially with paging support in mind...

I know... :) A Table component would be very nice. Let me know if I can help you (for example with table examples used in the real world).

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

I know... :) A Table component would be very nice. Let me know if I can help you (for example with table examples used in the real world).

In fact... that would be very helpful. Currently, I am working with the Table element implementation. It started as an experiment and implementing a simple table layout algorithm (without paging) was surprisingly easy. However, when I want to add a couple of helpers and life-quality improvements, things start to be complex. Not to mention that adding paging support already proves to be a corner-case hell :D

Please take a look at this example. Any suggestions regarding API and functionality are welcome 😁

.Table(table =>
{
    table.ColumnsDefinition(columns =>
    {
        columns.ConstantColumn(100);
        columns.RelativeColumn();
        columns.ConstantColumn(100);
        columns.ConstantColumn(200);
    });

    // now you need to provide exact positions of each cell
    // I want to implement automated cell placement
    // therefore, the Row() and Column() invocations will not be needed
    table.Cell().Row(1).Column(1).ColumnSpan(2).Element(CreateBox("A"));
    table.Cell().Row(1).Column(3).Element(CreateBox("B"));
    table.Cell().Row(1).Column(4).Element(CreateBox("C"));
    
    table.Cell().Row(2).Column(1).Element(CreateBox("D"));
    table.Cell().Row(2).RowSpan(2).Column(2).Element(CreateBox("E"));
    table.Cell().Row(2).RowSpan(3).Column(3).ColumnSpan(2).Element(CreateBox("F"));
    
    table.Cell().Row(3).RowSpan(2).Column(1).Element(CreateBox("G"));
    table.Cell().Row(4).RowSpan(2).Column(2).Element(CreateBox("H"));
    table.Cell().Row(5).Column(3).Element(CreateBox("I"));
    table.Cell().Row(5).Column(4).Element(CreateBox("J"));
    table.Cell().Row(5).RowSpan(2).Column(1).Element(CreateBox("K"));
    table.Cell().Row(6).Column(2).ColumnSpan(2).Element(CreateBox("L"));
    table.Cell().Row(6).Column(4).Element(CreateBox("M"));

    static Action<IContainer> CreateBox(string label)
    {
        return container =>
        {
            var height = Random.Next(2, 7) * 25;
            
            container
                .Border(1)
                .Background(Placeholders.BackgroundColor())
                .MinHeight(height)
                .AlignCenter()
                .AlignMiddle()
                .Text($"{label}: {height}");
        };
    }
});

image

from questpdf.

qcz avatar qcz commented on May 21, 2024

The API looks very nice!

I'm a little worried about auto placement. I'm more fond of explicit declarations and layout exceptions if cells are overlapping instead of unwanted line wraps if I make an error. However I can live with that. Maybe auto placement should be the default, but keep the ability to define exact position?

What about table headers? If there is a header area with similar capabilities as the main table area (eg. multiple rows, column & row span) then I can't think of any missing functionality I need right now.

And a final question: Cells can take any kind of content an Item can in a grid (e.g. Text, Image, Component, Row, Stack)?

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

I'm a little worried about auto placement. I'm more fond of explicit declarations and layout exceptions if cells are overlapping instead of unwanted line wraps if I make an error. However I can live with that. Maybe auto placement should be the default, but keep the ability to define exact position?

I think that this feature may be essential. With complex tables, with dozens of cells, describing each cell position seems tedious. Not to mention the frustration, when you ned to add cells in between (and everything moves). The library will attempt to auto-place cell only and only if Row and Column coordinates are not provided. Moreover, the auto-placed cell should be after the previous cell.

And a final question: Cells can take any kind of content an Item can in a grid (e.g. Text, Image, Component, Row, Stack)?

Table cells are just containers, they can take any type of content 😀 What about tables in tables?

What about table headers? If there is a header area with similar capabilities as the main table area (eg. multiple rows, column & row span) then I can't think of any missing functionality I need right now.

I think that the table implementation will not cover any specific rules regarding headers. Headers will just be cells with different content formatting. To make table headers visible on each page - you can just use the Decoration element 😀

from questpdf.

qcz avatar qcz commented on May 21, 2024

The library will attempt to auto-place cell only and only if Row and Column coordinates are not provided. Moreover, the auto-placed cell should be after the previous cell.

This is how I imagined. Perfect. Will it detect collision, e.g. if I put a cell over an existing cell?

Table cells are just containers, they can take any type of content 😀

Perfect, again.

What about tables in tables?

I now see where corner-case hells come from :D

I think that the table implementation will not cover any specific rules regarding headers. Headers will just be cells with different content formatting. To make table headers visible on each page - you can just use the Decoration element 😀

Reuse of the Decoration element for this is very clever.

I'm looking forward to it, it's looking very promising.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Will it detect collision, e.g. if I put a cell over an existing cell?

Good question. The algorithm that I am trying to implement should properly render cells even if they collide. It may be sometimes useful (although I don't know when yet). On the other hand, in vast majority of cases, we don't use overlapping cells and collisions may imply code issues - throwing an exception may help the developer.

I'm looking forward to it, it's looking very promising.

I will do my best to finish it before 2022.01 release. Would you like to help me with testing? I don't know yet how to write proper automated tests for this complex functionality so manual testing would be welcome 😅

from questpdf.

qcz avatar qcz commented on May 21, 2024

Of course. Let me know when there is a testable version.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

@qcz Good news! 😁I have just finished the implementation of the aforementioned table-layout algorithm. Can you please download the QuestPDF 2022.1.0-beta0 release and perform some testing? The implementation is currently available in the 2022.01 branch.

Good candidates to test:

  1. Cell placement algorithm. When the cell position is not hardcoded (via .Row(value) and .Column(value) method), the position of such cell is calculated automatically. Question for later: what should happen if only one coordinate is specified?
  2. Overall table rendering. Checking if everything is correctly placed, if rows are properly defined, if cell sizes look good.
  3. Paging support and corner cases. E.g. what if the element wants to partially rendered (text) or be wrapped to the next page (e.g. image?)

I would like to publish a new release on January 10th so any help next week is greatly appreciated!

Example code:

.Border(1)
.Table(table =>
{
    table.ColumnsDefinition(columns =>
    {
        columns.ConstantColumn(100);
        columns.RelativeColumn();
        columns.ConstantColumn(100);
        columns.RelativeColumn();
    });

    // .LabelCell() and .ValueCell() are DSL extension functions available only in the testing environment code
    // they apply custom style, e.g. border, padding, background, etc.
    table.Cell().RowSpan(3).LabelCell("Project");
    table.Cell().RowSpan(3).ShowEntire().ValueCell(Placeholders.Sentence());

    table.Cell().LabelCell("Report number");
    table.Cell().ValueCell("0");
    
    table.Cell().LabelCell("Date");
    table.Cell().ValueCell(Placeholders.ShortDate());

    table.Cell().LabelCell("Inspector");
    table.Cell().ValueCell("Marcin Ziąbek");

    table.Cell().ColumnSpan(2).LabelCell("Morning weather");
    table.Cell().ColumnSpan(2).LabelCell("Evening weather");

    table.Cell().ValueCell("Time");
    table.Cell().ValueCell("7:13");

    table.Cell().ValueCell("Time");
    table.Cell().ValueCell("18:25");

    table.Cell().ValueCell("Description");
    table.Cell().ValueCell("Sunny");

    table.Cell().ValueCell("Description");
    table.Cell().ValueCell("Windy");

    table.Cell().ValueCell("Wind");
    table.Cell().ValueCell("Mild");

    table.Cell().ValueCell("Wind");
    table.Cell().ValueCell("Strong");

    table.Cell().ValueCell("Temperature");
    table.Cell().ValueCell("17°C");

    table.Cell().ValueCell("Temperature");
    table.Cell().ValueCell("32°C");

    table.Cell().LabelCell("Remarks");
    table.Cell().ColumnSpan(3).ValueCell(Placeholders.Paragraph());
});

Produces:
image

from questpdf.

qcz avatar qcz commented on May 21, 2024

@MarcinZiabek Happy New Year and thanks for the notification! I'll look into it next week.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

@qcz I have a couple of updates 😁

QuestPDF 2022.1-beta1

  • fixed a bug with cell sizes

QuestPDF 2022.1-beta2

  • fixed a couple of table-related corner-cases,
  • added the ExtendLastCellsToTableBottom flag,
  • added a possibility to apply a default cell style via DefaultCellStyle (I am curious to hear your thoughts),
  • added more examples in the code.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

I have just updated the documentation to cover the Table element. Please take a look 😁

from questpdf.

qcz avatar qcz commented on May 21, 2024

@MarcinZiabek I've successfully ported the table generation portion of our reporting services using QuestPDF! Thank you for your work!

The API is very good and was able to render everything I had in my test suite. One example:
mstsc_2022-01-06_18-48-11

Paging is also fine, decoration with two tables (one for the header and one for the actual content) does the job for repeated headers. The only weird thing is that I have to define table columns twice if I use the decoration for table header and body (but I can live with that).

Oh, and one extra annoyance. Our system uses centimeters to define the width of the columns. Before I realized that I have to convert them to points by multiplying their value, I passed the centimeter values to QuestPDF and got infinite layout exception which I did not understand at first. I think the small column size resulted in text breaks (eg. one character in every line) and thus very-very large cells. Is it possible to detect this when rendering and providing a more specific error message?

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Paging is also fine, decoration with two tables (one for the header and one for the actual content) does the job for repeated headers. The only weird thing is that I have to define table columns twice if I use the decoration for table header and body (but I can live with that).

In this example, I have posted how you can reuse the same piece of code to configure columns in both tables. The same approach can be used for cell styling 😁

Oh, and one extra annoyance. Our system uses centimeters to define the width of the columns. Before I realized that I have to convert them to points by multiplying their value, I passed the centimeter values to QuestPDF and got infinite layout exception which I did not understand at first. I think the small column size resulted in text breaks (eg. one character in every line) and thus very-very large cells. Is it possible to detect this when rendering and providing a more specific error message?

When working in the DEBUG mode and the InfiniteLayoutException is thrown, it additionally contains an element trace that should provide a hint what is going on.

from questpdf.

qcz avatar qcz commented on May 21, 2024

In this example, I have posted how you can reuse the same piece of code to configure columns in both tables. The same approach can be used for cell styling 😁

I used a similar solution. It is just weird that I need to define it twice, that's all. But as I said, I can live with that :)

When working in the DEBUG mode and the InfiniteLayoutException is thrown, it additionally contains an element trace that should provide a hint what is going on.

Yeah, it was there but it did not help me at that time to discover the exact issue. Next time I'll look into it deeper.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Oh, and one extra annoyance. Our system uses centimeters to define the width of the columns. Before I realized that I have to convert them to points by multiplying their value, I passed the centimeter values to QuestPDF and got infinite layout exception which I did not understand at first. I think the small column size resulted in text breaks (eg. one character in every line) and thus very-very large cells. Is it possible to detect this when rendering and providing a more specific error message?

It is exactly what happens. Assuming your column is only 1 point wide, it is too narrow to fit even one character of text per line.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

@qcz What about some code simplification with QuestPDF 2022.01-beta4 and built-in support for table headers and footers? 😁

from questpdf.

qcz avatar qcz commented on May 21, 2024

Wow, very nice! Rewritten my solution using the new version and it looks much cleaner 🙂

People will love this new Table component!

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

I can only hope that I fixed all remaining issues 🙊

When preparing documentation examples, I found a couple of corner cases that required fixing. I consider the entire algorithm as quite complex and at this point, I have no idea how to properly test it. Up to now, I was trying to design everything as simple elements that were also simple to test. It is not possible with this algorithm and I will need to find a better strategy to handle such cases.

from questpdf.

qcz avatar qcz commented on May 21, 2024

I've run some real world tests and found a problem with paging.

If one or more cells contains more text than the others at the bottom of the page, they are properly paged to the next page. However the other cells do not, and on the next page, there are empty:

mstsc_2022-01-07_07-34-54

mstsc_2022-01-07_07-35-02

I expected that empty boxes are displayed on the next page (or the content if ShowOnce is not used), like in the ShowOnce documentation.

PS.: In the without ShowOnce examples, the second image should contain the same "Unde possimus" on the left column as the first image, so the sentence above it (as a result, left column was repeated twice) is true.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

I expected that empty boxes are displayed on the next page (or the content if ShowOnce is not used), like in the ShowOnce documentation.

The observed behaviour is actually my design choice to simplify the complexity a bit (a be able to apply one optimization). However, I see that it may not make sense in some cases. I decided to change it so it works exactly how you expect. New version is available with QuestPDF 2022.01-beta5 (hopefully the last prerelease in this cycle).

PS.: In the without ShowOnce examples, the second image should contain the same "Unde possimus" on the left column as the first image, so the sentence above it (as a result, left column was repeated twice) is true.

You're right, thank you for finding this issue. I've just fixed it 😀

from questpdf.

qcz avatar qcz commented on May 21, 2024

Thanks! It's working great!

One interesting thing which was not obvious from the documentation. When using ShowOnce, any styling defined before the ShowOnce will be present on the repeated element, but not what is defined after it. So the position of ShowOnce is important and affects rendering.

For example this shows an empty box with a border and background on the next page:

row.RelativeColumn()
        .Background(Colors.Grey.Lighten2)
        .Border(1)
        .Padding(5)
        .ShowOnce()
        .Text(Placeholders.Label());

But this does not:

row.RelativeColumn()
        .ShowOnce()
        .Background(Colors.Grey.Lighten2)
        .Border(1)
        .Padding(5)
        .Text(Placeholders.Label());

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

That's correct, the order of invocations is really important. This is the same situation as with function composition in functional programming (e.g. LINQ).

The same would apply for:

  1. .Padding(10).Background(Colors.Black).Width(100).Height(100)
  2. .Background(Colors.Black).Padding(10).Width(100).Height(100)

Both will produce rectangles but with different sizes. Do you have any ideas on how to describe it in a more obvious way?

(This is also a reason why I decided to put the ShowOnce element in the example - to at least give a hint regarding proper position in the code).

from questpdf.

qcz avatar qcz commented on May 21, 2024

Maybe you should dedicate a section in the docs explaining this with sample code & images.

For ShowOnce & the others, I think something like this would be good:

All calls coming after ShowOnce will run only once. Calls before ShowOnce will be repeated on every render where the item appears (eg. a secondary render after a page break). So if you want to render an empty box after page break, you must put Border and Background calls before ShowOnce.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

Yes, I agree. I will include soon :) I think that the next overhaul of documentation should better explain the concept of Fluent API invocation order. Something worth mentioning in the Getting Started tutorial 😁

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

This discussion helped introduce two improvements:

  1. We created the Table element. It covers more advanced use cases, e.g. when a column needs to take both constant and relative space. In such a case, the recommendation is to use the Table element instead of the Row element.
  2. I have created an additional documentation section explaining the important role of the Fluent API method execution order.

Once again, thank you for your help! 😁 Closing

from questpdf.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.