Draw my Contours

Given a rectangular matrix of elevations, draw its contours.

Two elements x and y are on the same contour level if floor(x/10) == floor(y/10). For example, 52 and 58 are on the same contour level, but 58 and 64 are not.

The act of drawing contours is defined as follows: For every element e, replace it with a two-char string chosen as follows:

• the first character is " " if the element below e is on the same contour level as e or if there is no element to the below e and "_" otherwise
• the second character is " " if the element to the right of e is on the same contour level as e or there is no element to the right of e and "|" otherwise

The elements within rows are joined together, then the rows are joined together with newlines.

Example

Let's say the input is [[5,20],[3,6]], visualized as

5 20
3 6


We first look at 5. Since 3 is on the same contour level as 5, the first character is " ". Since 20 is not on the same contour level as 5, the second character is "|".

Now we look at 20. Since 6 is not on the same contour level as 20, the first character is "_". Since there is no element to the right of 20, the second character is " ".

Now we look at 3. Since there is no element below 3, the first character is " ". Since 6 is on the same contour level as 3, the second character is " ".

Now we look at 6. Since there is no element below 6, the first character is " ". Since there is no element to the right of 6, the second character is " ".

Based on these two-char strings, we make replacements to get [[" |","_ "],[" "," "]]. Joining these together, we get an output of

 |_


Rules

• The input matrix will always be rectangular and composed of positive integers.
• Trailing spaces or newlines may be in any amount (including 0) and do not have to be consistent in any way.
• You do not have to follow the same algorithm as long as you produce the same results.
• Your program or function may output a newline-separated string, list of strings, or equivalent.
• This is , so shortest code in bytes wins.

Test Cases

input
output

[[1,5,8,9],[3,11,13,8],[7,14,10,9],[4,8,7,6]]
_ _
|   |
|_ _|

[[0,10,20,30,40,50,60,70,80,90],[0,0,10,10,20,20,30,30,40,40],[0,0,0,10,10,10,20,20,20,30],[0,0,0,0,10,10,10,10,20,20],[0,0,0,0,0,10,10,10,10,10],[0,0,0,0,0,0,10,10,10,10],[0,0,0,0,0,0,0,10,10,10],[0,0,0,0,0,0,0,0,10,10],[0,0,0,0,0,0,0,0,0,10],[0,0,0,0,0,0,0,0,0,0]]
|_|_|_|_|_|_|_|_|_
|_  |_ _|_ _|_ _
|_    |_ _  |_
|_      |_ _
|_
|_
|_
|_
|_

[[5,5,5,5,5,5,5,5,5,5,5],[5,10,10,10,10,10,10,10,10,10,5],[5,10,15,15,15,15,15,15,15,10,5],[5,10,15,20,20,20,20,20,15,10,5],[5,10,15,20,25,25,25,20,15,10,5],[5,10,15,20,25,30,25,20,15,10,5],[5,10,15,20,25,25,25,20,15,10,5],[5,10,15,20,20,20,20,20,15,10,5],[5,10,15,15,15,15,15,15,15,10,5],[5,10,10,10,10,10,10,10,10,10,5],[5,5,5,5,5,5,5,5,5,5,5]]
_ _ _ _ _ _ _ _ _
|                 |
|    _ _ _ _ _    |
|   |         |   |
|   |    _    |   |
|   |   |_|   |   |
|   |         |   |
|   |_ _ _ _ _|   |
|                 |
|_ _ _ _ _ _ _ _ _|

[[35,32,29,26,25,25,25,26,29,32,35],[32,28,25,22,20,20,20,22,25,28,32],[29,25,21,18,15,15,15,18,21,25,29],[26,22,18,14,11,10,11,14,18,22,26],[25,20,15,11,7,5,7,11,15,20,25],[25,20,15,10,5,0,5,10,15,20,25],[25,20,15,11,7,5,7,11,15,20,25],[26,22,18,14,11,10,11,14,18,22,26],[29,25,21,18,15,15,15,18,21,25,29],[32,28,25,22,20,20,20,22,25,28,32],[35,32,29,26,25,25,25,26,29,32,35]]
_|             |_
_|    _ _ _ _ _    |_
_|         |_
|    _ _ _    |
|   |     |   |
|   |     |   |
|   |_ _ _|   |
|_           _|
_    |_ _ _ _ _|    _
|_               _|
|             |

• I don't even begin to read this before realizing how cool this will be
– user63187
Sep 14, 2017 at 23:37

Perl 6, 135 bytes (131 chars)

{my$n="_";sub w{$^a.chop-$^b.chop??$n!!" "};my&q={|.[1..*],.tail};(($_ «[&w]».&q) ZZ~{$n="|";$_ «[&w]».map(*.&q)}()).map:{say |$_}}


Try it online!

Slightly ungolfed:

{
my $n = "_"; sub w {$^a.chop - $^b.chop ??$n !! " "};
my &q = {|.[1..*],.tail};
(
($_ «[&w]».&q) ZZ~ {$n="|";$_ «[&w]».map(*.&q)}() ).map:{say |$_}
}


Explanation: First, we define a variable $n (line 2) and a function w (line 3). This function returns a space if its two arguments are at the same "elevation", and the contents of variable $n otherwise. Instead of dividing by 10 and flooring, we abuse the fact that ints are Cool (can be treated like strings) and use chop to remove the last character (=digit). Then we calmly subtract them, forcing them into numbers again :—).

After that (line 4) we make up a function q that takes a list and returns that list with first element removed and the last element duplicated.

On the next 3 lines, we're going to make 2 more matrices from the input matrix: the first one has the first row missing and the last row duplicated (that's just .&q — using .&, you can call a function on anything as if it was a method — the thing in front of the dot is then the first argument), the other one has the first column missing and the last column duplicated (that's .map(*.&q)).

First (line 4) we take the original matrix $_, "overlay" it with the "shifted rows" matrix and use the function w as a binary operator (that's that [&w]) on the matching elements. That one puts a _ wherever the matching elements are on the different elevations, and a otherwise. So we get ½ of the result (only the "first characters"). At line 6, we do the same thing, but first we change $n to |, and we now "overlay" the original matrix with the matrix with shifted columns. Result has a | at different and at the same elevations. These are the "second characters".

Now we just combine them together. We zip the arrays with a zip with a concat (yeah...), which results in a matrix of the original shape whose each element is the 2 matching elements of the "half-solutions" concatenated. Finally, we just map over this matrix (which is really a list of lists). Each of these lists is flattened and then say-ed (printed with a newline). Since say can take any number of arguments and it prints them all without separators, making the newline only at the end, we get the desired picture on stdout. (And the block returns a list of Trues (each say returns one True), but who cares.)

• +1 for but who cares Sep 15, 2017 at 2:20

Jelly,  25 23  22 bytes

-1 byte thanks to miles (I vectorises)

:⁵I;€0ao⁶
Zç”_Zż"ç”|$Y  A full program printing the result. As a monadic link it takes a list of lists of numbers, the elevations, and returns a list lists, however these "rows" consist of lists of two character "pairs" - if this is fine then 1 byte may be saved by removing Y. Try it online! How? :⁵I;€0ao⁶ - Link 1, assignCharacters (row-wise): list of lists of numbers; character, c ⁵ - literal 10 : - integer division (vectorises) I - incremental differences (vectorises) (zero if the same else non-zero) 0 - literal 0 ;€ - concatenate for €ach (rightmost edge of a row has no contour mark) a - logical and (vectorises) with c (replace non-zeros with the contour character) ⁶ - literal space character o - logical or (vectorises) (replace the zeros with spaces) Zç”_Zż"ç”|$Y - Main link: list of lists of numbers, contours
Z            - transpose the input (get the columns)
”_         - literal underscore character, '_'
ç           - call the last link (1) as a dyad with '_'
Z        - transpose the result
$- last two links as a monad: ”| - literal pipe character, '|' ç - call the last link (1) as a dyad with '|' " - zip with the dyadic operation: ż - zip (interleave the column-wise characters with the row-wise ones) Y - join with newlines - implicit print  • grr 3 bytes. +1 but I will try to outgolf you ;) Sep 15, 2017 at 2:32 • Independent solution - just saw yours is very similar! ⁵ saves you one straight off... Sep 15, 2017 at 2:33 • You can save a byte using each on the join in the helper :⁵I;€0ao⁶ instead of in the main link Zç”_Zż"ç”|$Y Sep 15, 2017 at 10:14
• @miles Oh wow, that works? Thanks! I imagined I would not vectorise like that. Sep 15, 2017 at 10:19
• Yeah I vectorizes at depth 1, and both a and o vectorize at depth 0 Sep 15, 2017 at 10:25

Python 2, 199186157 155 bytes

lambda a:(lambda x:'\n'.join(''.join('_ '[x==z]+'| '[x==y]for x,y,z in zip(r,r[1:]+r[-1:],q))for r,q in zip(x,x[1:]+x[-1:])))([[v/10for v in r]for r in a])


Try it online!

Jelly, 24 bytes

:⁵IṠ;€0
ZÇZị⁾_ +³Ç¤ị⁾| ¤


Try it online!

Explanation

:⁵IṠ;€0           Helper Link; get contour data
:                 Floor division by
⁵                10
I               Compute increments
Ṡ              Sign; ±1 for different values and 0 for same values
;             Append
0           Zero
€            To each row
Z                 Zip the input (for vertical contours _)
Ç                Get the contour data
Z               Zip the data (because it's zipped from the first Z)
ị              Index into the string
⁾_            "_ "
³ ¤    ¤  Nilad starting from (input)
Ç        Get contour data (horizontal contours |)
ị      Index into the string
⁾|    "| "


-2 bytes thanks to Jonathan Allan

• Your solution can actually do away with the Y - it will return a list of lists of characters, which I believe is OK (while mine has pairs inside the "rows"). Sep 15, 2017 at 2:36
• @JonathanAllan oh yeah true... thanks! Sep 15, 2017 at 2:38

Python 2, 226 bytes

l=[[j/10for j in i]for i in input()]
for i,j in enumerate(l[:-1]):print''.join('_ '[h==l[i+1][g]]+'| '[h==j[g+1]]for g,h in enumerate(j[:-1]))+'_ '[j[-1]==l[i+1][-1]]
print''.join(' '+'| '[i==j]for i,j in zip(l[-1],l[-1][1:]))


Try it online!

Whew, that was a doozy to work the logic out. I now see Hyper Neutrino ninja'd me with a shorter answer but I spent too much work on this not to post it. :P

Also, can I just say, this is an awesome way to make ASCII-art. Excuse me while I make a boatload more of these.

• > ninja'd: dude it's been 45 minutes Sep 15, 2017 at 0:31
• Yeah, I didn't look at the answers... :P Sep 15, 2017 at 0:31
• You can save 4 bytes by defining a variable for enumerate instead of using the full name twice. Sep 15, 2017 at 1:19
• 218 bytes by removing the first enumerate (obs, i had to remove some inputs to be able to link it here) Sep 15, 2017 at 13:01

J, 58 bytes

f=.{~0==/@]
[:(,/"2)2 2((' _'f{."1),' |'f{.);.3 1:+<.@%&10


Try it online!

An anonymous function which takes a matrix and outputs the contours.

Lotta room for improvement here. I didn't have time to try all the test cases so let me know if there are any issues. Will try to golf more and explain later.

(Quick) Explanation

Helper function: indexes into a length 2 string based on whether the first element of a 2 length array is equal to the second. If it is equal, it indexes into the zeroth element, if it is unequal, it indexes into the first. A 1 length array always indexes into the zeroth element of the string.

f=.{~0==/@]


Main function

[:(,/"2)2 2((' _'f{."1),' |'f{.);.3 1:+<.@%&10


1:+<.@%&10 floors each element divided by 10 and adds 1 (so we will never get 0 -- this is important for the helper function).

2 2((' _'f{."1),' |'f{.);.3 cuts the matrix into 2 x 2 segments if it can (otherwise it'll give a 2 x 1, 1 x 2, or 1 x 1 segment near the edges) and applies the function that uses f to compare the top left element to the top right and the top left element to the bottom left.

(,/"2) flattens the result into the desired shape. I really feel like I should be able to avoid having to use this (and a lot of other things, but I digress).

J, 46 45 bytes

' _|'({~];"1@(,&.>-@|:)&(0,~}.~:}:)|:)<.@%&10


Try it online!

JavaScript (ES6), 120 118 bytes

a=>a.map(b=>b.map(c=>c/10|0)).map((b,i,a)=>b.map((c,j)=>((a[i+1]||0)[j]-c?'_':' ')+(b[j+1]-c?'|':' ')).join).join\n


Where \n represents the literal newline character. Edit: Saved 2 bytes thanks to @Bálint.

f=
a=>a.map(b=>b.map(c=>c/10|0)).map((b,i,a)=>b.map((c,j)=>((a[i+1]||0)[j]-c?'_':' ')+(b[j+1]-c?'|':' ')).join).join

;[
[[5,20],[3,6]]
,
[[1,5,8,9],[3,11,13,8],[7,14,10,9],[4,8,7,6]]
,
[[0,10,20,30,40,50,60,70,80,90],[0,0,10,10,20,20,30,30,40,40],[0,0,0,10,10,10,20,20,20,30],[0,0,0,0,10,10,10,10,20,20],[0,0,0,0,0,10,10,10,10,10],[0,0,0,0,0,0,10,10,10,10],[0,0,0,0,0,0,0,10,10,10],[0,0,0,0,0,0,0,0,10,10],[0,0,0,0,0,0,0,0,0,10],[0,0,0,0,0,0,0,0,0,0]]
,
[[5,5,5,5,5,5,5,5,5,5,5],[5,10,10,10,10,10,10,10,10,10,5],[5,10,15,15,15,15,15,15,15,10,5],[5,10,15,20,20,20,20,20,15,10,5],[5,10,15,20,25,25,25,20,15,10,5],[5,10,15,20,25,30,25,20,15,10,5],[5,10,15,20,25,25,25,20,15,10,5],[5,10,15,20,20,20,20,20,15,10,5],[5,10,15,15,15,15,15,15,15,10,5],[5,10,10,10,10,10,10,10,10,10,5],[5,5,5,5,5,5,5,5,5,5,5]]
,
[[35,32,29,26,25,25,25,26,29,32,35],[32,28,25,22,20,20,20,22,25,28,32],[29,25,21,18,15,15,15,18,21,25,29],[26,22,18,14,11,10,11,14,18,22,26],[25,20,15,11,7,5,7,11,15,20,25],[25,20,15,10,5,0,5,10,15,20,25],[25,20,15,11,7,5,7,11,15,20,25],[26,22,18,14,11,10,11,14,18,22,26],[29,25,21,18,15,15,15,18,21,25,29],[32,28,25,22,20,20,20,22,25,28,32],[35,32,29,26,25,25,25,26,29,32,35]]
].forEach(a=>document.write(['<pre>','</pre>'].join(f(a))));

• You can turn the (a[i] || [])[j] constructs to (a[i] || 0)[j] Sep 15, 2017 at 22:33
• Also, in the last join\n you could remove the \n part and replace it with an actual newline Sep 15, 2017 at 22:36
• 114 bytes: a=>a.map(b=>b.map(c=>c/10|0)).map((b,i,a)=>b.map((c,j)=>" _"[(a[i+1]||0)[j]-c&1]+" |"[b[j+1]-c&1]).join).join<new line here> Sep 15, 2017 at 22:52
• @Bálint Bah, I keep forgetting to do that \n part; I test in a REPL so literal newlines would get in the way.
– Neil
Sep 15, 2017 at 23:00
• @Bálint But your last suggestion fails for the original example, which I've added to the list of outputs.
– Neil
Sep 16, 2017 at 0:07

Proton, 202 bytes

R=(L=len)+range
k=[map((//)&10,r)for r:eval(input())]
d=(x,y,X,Y)=>X>=L(k)or Y>=L(k[X])or k[x][y]==k[X][Y]
print('\n'.join(map(''.join,[['_ '[d(x,y,x+1,y)]+'| '[d(x,y,x,y+1)]for y:R(k[x])]for x:R(k)])))


Try it online!

-2 bytes thanks to Jonathan Frech
-15 bytes by switching to Proton instead of Python 2

• You can save two bytes by replacing len with Land defining L=len;. Sep 15, 2017 at 1:15

Java 8, 200170 169 bytes

a->{String r="";for(int l=a.length,i=0,j;i<l;i++,r+="\n")for(j=0;j<l;r+=(i>l-2||a[i][j]/10==a[i+1][j]/10?" ":"_")+(j++>l-2||a[i][j-1]/10==a[i][j]/10?" ":"|"));return r;}


Explanation:

Try it here.

Note that integer division in Java automatically floors.

a->{                   // Method with 2D int-array as parameter and String return-type
String r="";         //  Result-String
for(int l=a.length,  //  Length of the input array
i=0,j;           //  Index integers
i<l;i++,         //  Loop (1) over the rows of the input array
r+="\n")     //  and append a new-line to the result after every iteration
for(j=0;j<l;       //   Inner loop (2) over the columns of a row
r+=              //    Append the String with:
(i>l-2        //      If it's the last row,
||a[i][j]/10==a[i+1][j]/10?
//      or the current and next rows are equal floored/10:
" "          //       Use a space
:             //      Else:
"_")         //       Use a "_"
+              //     Plus
(j++>l-2      //      If it's the last column in the row,
||a[i][j-1]/10==a[i][j]/10?
//      or the current and next columns are equal floored/10:
" "          //       Use a space
:             //      Else:
"|")         //       Use "|"
);                 //   End of column loop (2)
//  End of row-loop (1) (implicit / single-line body)
return r;            //  Return the result-String
}                      // End of method


R, 159 bytes

f=function(m){M=m%/%10;a=cbind(0,t(apply(M,1,diff)));b=rbind(apply(M,2,diff),0);a[!!a]="|";b[!!b]="_";M[]=gsub("0"," ",paste0(a,b));write(t(M),"",ncol(m),,"")}


With newlines and indentations:

f=function(m){
M=m%/%10
a=cbind(0,t(apply(M,1,diff))) #row-wise difference
b=rbind(apply(M,2,diff),0) #column-wise difference
a[!!a]="|"
b[!!b]="_"
M[]=gsub("0"," ",paste0(a,b)) # M[] is a trick to force the result to have the same structure as M
write(t(M),"",ncol(m),,"")
}


Does the integer division of the matrix, measures the row-wise and column-wise differences, and when not null replace by |and _ respectively, then pastes both (painless, thanks to R's vectorization) and outputs.

Test cases:

> m=matrix(c(0,10,20,30,40,50,60,70,80,90,0,0,10,10,20,20,30,30,40,40,0,0,0,10,10,10,20,20,20,30,0,0,0,0,10,10,10,10,20,20,0,0,0,0,0,10,10,10,10,10,0,0,0,0,0,0,10,10,10,10,0,0,0,0,0,0,0,10,10,10,0,0,0,0,0,0,0,0,10,10,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0),byrow=T,ncol=10)
> f(m)
|_|_|_|_|_|_|_|_|_
|_  |_ _|_ _|_ _
|_    |_ _  |_
|_      |_ _
|_
|_
|_
|_
|_

> m=matrix(c(5,5,5,5,5,5,5,5,5,5,5,5,10,10,10,10,10,10,10,10,10,5,5,10,15,15,15,15,15,15,15,10,5,5,10,15,20,20,20,20,20,15,10,5,5,10,15,20,25,25,25,20,15,10,5,5,10,15,20,25,30,25,20,15,10,5,5,10,15,20,25,25,25,20,15,10,5,5,10,15,20,20,20,20,20,15,10,5,5,10,15,15,15,15,15,15,15,10,5,5,10,10,10,10,10,10,10,10,10,5,5,5,5,5,5,5,5,5,5,5,5),byrow=T,ncol=11)
> f(m)
_ _ _ _ _ _ _ _ _
|                 |
|    _ _ _ _ _    |
|   |         |   |
|   |    _    |   |
|   |   |_|   |   |
|   |         |   |
|   |_ _ _ _ _|   |
|                 |
|_ _ _ _ _ _ _ _ _|



Perl 5, 130 126 bytes

124 bytes of code + 2 for -ap flags

push@a,[map 0|$_/10,@F]}{map{say map{($a[$r+1][$c]-$_&&$r<$#a?'_':$").($a[$r][++$c]-$_&&$c<@{$a[0]}?'|':$")}@$_;$c=0;$r++}@a


Try it online!

Input format is 2-D grid of space separated numbers.

Explanation

This is from a previous iteration of the code.

push@a,[map 0|$_/10,@F] # read the input, divide it by 10, and store it in a 2-D array }{ # end the implicit while loop and start the final block map{ # repeat this for each line$_=($a[$r+1][$c]-$_&&$r<$#a?'_':$") # set appropriate characters to output based .($a[$r][++$c]-$_&&$c<@{$a[0]}?'|':$") # on the given rules
for@$_; # repeat for each number on the line$c=0;$r++; # setup row and column counters for next iteration say@$_                             # output this line
}@a