Visualize an Array

JavaScript (ES6), 223 203 bytes

f=(a,g=a=>a[0].map?`<${a.map(g).join`|`}>`:a.join` `,s=g([a]),r=[s],t=s.replace(/<[ -9|]*>|[ -9]/g,s=>s[1]?s.replace(/./g,c=>c>`9`?`+`:`-`):` `))=>t<`+`?r.join`\n`.replace(/<|>/g,`|`):f(a,g,t,[t,...r,t])

Port of @MitchSchwartz's Ruby solution. Previous version which worked by recursively wrapping the arrays (and therefore worked for arbitrary content, not just integers):

f=(...a)=>a[0]&&a[0].map?[s=`+${(a=a.map(a=>f(...a))).map(a=>a[0].replace(/./g,`-`)).join`+`}+`,...[...Array(Math.max(...a.map(a=>a.length)))].map((_,i)=>`|${a.map(a=>a[i]||a[0].replace(/./g,` `)).join`|`}|`),s]:[a.join` `]

Note: Although I'm using the spread operator in my argument list, to obtain the desired output, provide a single parameter of the original array rather then trying to spread the array; this has the effect of wrapping the output in the desired outer box. Sadly the outer box costs me 18 bytes, and space-separating the integers costs me 8 bytes, otherwise the following alternative visualisation would suffice for 197 bytes:

f=a=>a.map?[s=`+${(a=a.map(f)).map(a=>a[0].replace(/./g,`-`)).join`+`}+`,...[...Array(Math.max(0,...a.map(a=>a.length)))].map((_,i)=>`|${a.map(a=>a[i]||a[0].replace(/./g,` `)).join`|`}|`),s]:[``+a]

Dyalog APL, 56 bytes

Thanks to ngn for helping removing about a third of the bytes.

{⍵≡∊⍵:⍉⍪⍉⍕⍵⋄(⊢,⊣/)⊃,/(1⊖('++','|'⍴⍨≢),'-'⍪⍣2↑)¨↓↑↓¨∇¨⍵}⊂

TryAPL

Define the function, then run each test case and compare to the built-in ]Display utility.
[1, 2, 3]
[[1, 2, 3], [4, 5], [6, 7, 8]]
[[[1, 2, 3], [4, 5]], [6, 7, 8]]
[]
[[], []]
[[], [1], [], [2], [], [3], []]
[[[[[0]]]]]
[[[[[4, 3, 2, 1]]]], [[[3, 2, 1]]], [[2, 1]], [1]]

Explanation

Overall, this is an anonymous function {...} atop an enclose . The latter just adds another level of nesting prompting the former to add an outer frame.

The anonymous function with white-space ( is the statement separator):

{
    ⍵ ≡ ∊⍵: ⍉ ⍪ ⍉ ⍕ ⍵
    (⊢ , ⊣/) ⊃ ,/ (1 ⊖ ('++' , '|' ⍴⍨ ≢) , '-' ⍪⍣2 ↑)¨ ↓ ↑ ↓¨ ∇¨ ⍵
}

Here it is again, but with separated utility functions:

CloseBox ← ⊢ , ⊣/
CreateVertical ← '++' , '|' ⍴⍨ ≢
AddHorizontals ← 1 ⊖ CreateVertical , '-' ⍪⍣2 ↑
{
    ⍵ ≡ ∊⍵: ⍉ ⍪ ⍉ ⍕ ⍵
    CloseBox ⊃ ,/ AddHorizontals¨ ↓ ↑ ↓¨ ∇¨ ⍵
}

Now let me explain each function:

CloseBox takes a table and returns the same table, but with the table's first column appended on the right of the table. Thus, given the 1-by-3 table XYZ, this function returns the 1-by-4 table XYZX, as follows:
 the argument (lit. what is on the right)
, prepended to
⊣/ the leftmost column (lit. the left-reduction of each row)

CreateVertical takes a table and returns the a string consisting of the characters which would fit |s on the sides of the table, but with two +s prepended to match two rows of -. Eventually the table will be cyclically rotated one row to get a single +---... row above and below. Thus, given any three row table, this function returns ++|||, as follows:
'++' , two pluses prepended to
'|' ⍴⍨ a stile reshaped by
 the (rows') tally of the argument

AddHorizontals takes a list-of-lists, makes it into a table, adds two rows of -s on top, adds the corresponding left edge characters on the left, then rotates one row to the bottom, so that the table has a border on the top, left, and bottom. As follows:
1 ⊖ rotate one row (the top row goes to the bottom) of
CreateVertical , the string ++|||... prepended (as a column) to
'-' ⍪⍣2 minus added twice to the top of
 the argument transformed from list-of-lists to table

{The anonymous function}: If the argument is a simple (not nested) list, make it into a character table (thus, given the 3-element list 1 2 3, this function returns the visually identical 1-by-five character table 1 2 3). If the argument is not a simple list, ensure the elements are simple character tables; pad them to equal height; frame each on their top, bottom, and left; combine them; and finally take the very first column and add it on the right. As follows:
{ begin the definition of an anonymous function
  ⍵ ≡ ∊⍵: if the argument is identical to the flattened argument (i.e. it is a simple list), then:
    transpose the
    columnized
    transposed
   ⍕ ⍵ stringified argument; else:
  CloseBox Add the leftmost column to the right of
  ⊃ ,/ the disclosed (because reduction encloses) concatenated-across
  AddHorizontals¨ add -s on top and bottom of each of
  ↓ ↑ ↓¨ the padded-to-equal-height* of
  ∇¨ ⍵  this anonymous function applied to each of the arguments
} end the definition of the anonymous function
* Lit. make each table into a list-of-lists, combine the lists-of-lists (padding with empty strings to fill short rows) into a table, then split the table into a list of lists-of-lists


Brainfuck, 423 bytes

->>+>>,[[>+>+<<-]+++++[>--------<-]>[<+>-[[-]<-]]>[[-]<<[>>>>+<<<<<<-<[>-<-]>>>-
]<<<[-<<<<<<-<]>+>>>]<<<[>>+>>>>>+<<<<<<<-]>>>>>>>>>,]<<+[<<,++>[-[>++<,<+[--<<<
<<<<+]>]]<[-<+]->>>>[<++<<[>>>>>>>+<<<<<<<-]>>>-[<++>-[>>>>+<<<<<++<]<[<<]>]<[>>
+<<<<]>>>+>+>[<<<-<]<[<<]>>>>->+>[-[<-<-[-[<]<[<++<<]>]<[<++++<<]>]<[>+<-[.<<<,<
]<[<<]]>]<[-<<<<<]>>[-[<+>---[<<++>>+[--[-[<+++++++<++>>,]]]]]<+++[<+++++++++++>
-]<-.,>>]>>>>+>>>>]<<-]

Formatted with some comments:

->>+>>,
[
  [>+>+<<-]
  +++++[>--------<-]
  >
  [
    not open paren
    <+>-
    [
      not paren
      [-]<-
    ]
  ]
  >
  [
    paren
    [-]
    <<
    [
      close paren
      >>>>+<<<<
      <<-<[>-<-]>>>
      -
    ]
    <<<
    [
      open paren directly after close paren
      -<<<<<<-<
    ]
    >+>>>
  ]
  <<<[>>+>>>>>+<<<<<<<-]>>>
  >>>>>>,
]
<<+
[
  <<,++>
  [
    -
    [
      >++<
      ,<+[--<<<<<<<+]
      >
    ]
  ]
  <[-<+]
  ->>>>
  [
    <++<<[>>>>>>>+<<<<<<<-]>>>-
    [
      at or before border
      <++>-
      [
        before border
        >>>>+<<<<
        <++<
      ]
      <[<<]
      >
    ]
    <
    [
      after border
      >>+<<
      <<
    ]
    >>>+>+>
    [
      column with digit or space
      <<<-<
    ]
    <[<<]
    >>>>->+>
    [
      middle or bottom
      -
      [
        bottom
        <-<-
        [
          at or before border
          -
          [
            before border
            <
          ]
          <
          [
            at border
            <++<<
          ]
          >
        ]
        <
        [
          after border
          <++++<<
        ]
        >
      ]
      <
      [
        middle
        >+<
        -[.<<<,<]
        <[<<]
      ]
      >
    ]
    <[-<<<<<]
    >>
    [
      border char or space
      -
      [
        not space
        <+>---
        [
          not plus
          <<++>>
          +
          [
            --
            [
              -
              [
                pipe
                <+++++++<++>>,
              ]
            ]
          ]
        ]
      ]
      <+++[<+++++++++++>-]<-.,>>
    ]
    > >>>+>>>>
  ]
  <<-
]

Try it online.

Expects input formatted like (((((4 3 2 1))))(((3 2 1)))((2 1))(1)) with a trailing newline, and produces output of the form:

+---------------------------------+
|+-------------+---------+-----+-+|
||+-----------+|+-------+|+---+| ||
|||+---------+|||+-----+|||   || ||
||||+-------+|||||     ||||   || ||
|||||4 3 2 1||||||3 2 1||||2 1||1||
||||+-------+|||||     ||||   || ||
|||+---------+|||+-----+|||   || ||
||+-----------+|+-------+|+---+| ||
|+-------------+---------+-----+-+|
+---------------------------------+

The basic idea is to calculate which character to print based on the depth of nesting. The output format is such that the row index of a box's top border is equal to the corresponding array's depth, with symmetry across the middle row.

The tape is divided into 7-cell nodes, with each node representing a column in the output.

The first loop consumes the input and initializes the nodes, keeping track of depth and whether the column corresponds to a parenthesis (i.e., whether the column contains a vertical border), and collapsing occurrences of )( into single nodes.

The next loop outputs one row per iteration. Within this loop, another loop traverses the nodes and prints one character per iteration; this is where most of the work takes place.

During the initialization loop, the memory layout of a node at the beginning of an iteration is

x d 0 c 0 0 0

where x is a boolean flag for whether the previous char was a closing parenthesis, d is depth (plus one), and c is the current character.

During the character printing loop, the memory layout of a node at the beginning of an iteration is

0 0 d1 d2 c p y

where d1 indicates depth compared with row index for top half; d2 is similar to d1 but for bottom half; c is the input character for that column if digit or space, otherwise zero; p indicates phase, i.e. top half, middle, or bottom half; and y is a flag that gets propagated from left to right, keeping track of whether we have reached the middle row yet. Note that since y becomes zero after processing a node, we can use the y cell of the previous node to gain more working space.

This setup allows us to avoid explicitly calculating the max depth during the initialization phase; the y flag is back-propagated to update the p cells accordingly.

There is a -1 cell to the left of the nodes to facilitate navigation, and there is a cell to the right of the nodes that keeps track of whether we have printed the last row yet.